Compare commits

..

86 Commits
1.6.6 ... 1.7.1

Author SHA1 Message Date
johan12345
ca6ff94c1f Github Actions: fix version code extraction from build.gradle.kts 2023-11-05 22:53:23 +01:00
Hosted Weblate
c02c259162 Translated using Weblate (German)
Currently translated at 100.0% (355 of 355 strings)

Co-authored-by: nautilusx <translate@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2023-11-05 22:41:19 +01:00
johan12345
a44718ded2 build.gradle.kts: Fix API key extraction 2023-11-05 22:38:52 +01:00
johan12345
4f268f5e83 build.gradle.kts: Fix API key extraction 2023-11-05 22:34:44 +01:00
johan12345
99b4841545 build.gradle.kts: Fix API key extraction 2023-11-05 22:21:38 +01:00
johan12345
7ad7d7da30 Release 1.7.1 2023-11-05 22:14:31 +01:00
johan12345
0e80f2bf82 Add long-press to copy charger address/coordinates 2023-11-05 21:43:40 +01:00
johan12345
b5a6ceb5f9 rework favorites deletion undo logic to fix #304 2023-11-05 20:53:33 +01:00
Johan von Forstner
c655cae405 Tesla API: fix sorting
refs e7b42e2c
2023-11-04 18:51:26 +01:00
Johan von Forstner
fb2e510220 update other dependencies 2023-11-04 17:03:21 +01:00
Johan von Forstner
c170270557 update car app library 2023-11-04 16:55:15 +01:00
Johan von Forstner
e7b42e2c19 Tesla: charger label is now nullable 2023-11-04 16:45:00 +01:00
johan12345
fc85e631c9 ensure Open Source licenses with AboutLibraries 2023-10-26 12:15:20 +02:00
johan12345
7192c9ebfa update AboutLibraries 2023-10-26 12:15:20 +02:00
Johan von Forstner
c652265ea1 Update Android Auto docs
fixes #303 (Android Auto for phone screens app was deprecated, now there is a new way to access settings)
2023-10-22 19:05:16 +02:00
johan12345
0320238dc9 add @Jean-BaptisteC to contributors 2023-10-19 16:48:48 +02:00
johan12345
e814c088bf replace deprecated Gradle property
fixes #302
2023-10-19 16:47:00 +02:00
Jean-Baptiste
b60b2d70b9 Update Github workflows (#301) 2023-10-19 16:38:27 +02:00
johan12345
3905656ea7 fix some lint warnings 2023-10-18 15:23:07 +02:00
johan12345
1f88e5fbdd fix some lint warnings 2023-10-18 15:09:50 +02:00
johan12345
646469e9ea update Gradle plugin 2023-10-18 13:24:00 +02:00
Jean-Baptiste
cce7c69d74 Migrate to build kotlin script (#298)
* Migrate to build KTS

* fix version code

* fix API keys

* fix kotlinVersion <-> kotlin_version

* fix supportedLocales

* Migrate to build KTS

* upgrade to Gradle 8.4

* updates

* variable renaming

* fix packagingOptions

---------

Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
2023-10-18 12:54:24 +02:00
johan12345
bc91c0571b Chargeprice: Fix switching between vehicles 2023-10-14 19:28:54 +02:00
johan12345
a83102a97e Tesla API: fix nullability 2023-10-14 19:14:07 +02:00
johan12345
f52a98540c Tesla API: add missing waiting estimate bucket 2023-10-14 19:11:42 +02:00
johan12345
e0d97e7219 fix NPE in PlaceSearchScreen 2023-10-14 19:10:16 +02:00
johan12345
3bbd20a57e possibly fix IllegalStateException 2023-10-14 19:05:39 +02:00
Hosted Weblate
3279c5eceb Translated using Weblate (Portuguese)
Currently translated at 100.0% (355 of 355 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-10-14 12:25:53 +02:00
Johan von Forstner
03d958ac2c Chargeprice: possibly fix IllegalStateException when switching vehicles 2023-10-07 20:10:50 +02:00
Johan von Forstner
b1fd370101 OpenChargeMap: use map.openchargemap.io as link
id query parameter added since
3330c9de96

#236
2023-10-07 19:36:19 +02:00
johan12345
bdc96fcd57 Android Auto: use simpler navigation intent URI without POI name
Workaround for TomTom GO, which seems to not handle intents with description correctly
2023-09-27 22:00:49 +02:00
johan12345
0d54e17eb4 update version code to retry Play Store release 2023-09-26 22:27:56 +02:00
johan12345
b1d0081fb7 re-enable CarAppTest
see https://github.com/robolectric/robolectric/issues/8404#issuecomment-1733309468
2023-09-26 17:47:03 +02:00
johan12345
1134499532 Release 1.7.0 2023-09-23 18:31:10 +02:00
johan12345
0417ade802 Android Auto: fix crash on Android 14 due to missing permission 2023-09-23 18:23:39 +02:00
johan12345
8fafabf6a8 update car app library 2023-09-21 19:24:33 +02:00
Hosted Weblate
1b3c35e94f Translated using Weblate (Portuguese)
Currently translated at 100.0% (355 of 355 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-09-20 19:56:03 +02:00
Hosted Weblate
23a3adc500 Translated using Weblate (German)
Currently translated at 99.7% (354 of 355 strings)

Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2023-09-16 23:09:42 +02:00
Hosted Weblate
16c2dcc938 Translated using Weblate (Norwegian Bokmål)
Currently translated at 80.2% (284 of 354 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2023-09-16 23:04:37 +02:00
Hosted Weblate
f322974e52 Translated using Weblate (Portuguese)
Currently translated at 100.0% (354 of 354 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-09-16 23:04:37 +02:00
Hosted Weblate
50ae2123e9 Translated using Weblate (English)
Currently translated at 100.0% (354 of 354 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/en/
Translation: EVMap/Android
2023-09-16 23:04:36 +02:00
johan12345
72894399f6 adjustments for Android Auto 2023-09-16 22:59:10 +02:00
johan12345
77014d754f implement Tesla Supercharger cost in AA/AAOS 2023-09-16 22:59:10 +02:00
johan12345
66dbd6426f implement Tesla Login for Android Auto/AAOS 2023-09-16 22:59:10 +02:00
johan12345
e4127f4a56 improve prediction graph generation 2023-09-16 22:59:10 +02:00
johan12345
f9bf8b80f7 Android Auto/AAOS: Add availability prediction 2023-09-16 22:59:10 +02:00
johan12345
67eeb47d5f update dependencies 2023-09-16 13:01:55 +02:00
johan12345
3c6a7cd536 make navController.navigate() calls safe
https://nezspencer.medium.com/navigation-components-a-fix-for-navigation-action-cannot-be-found-in-the-current-destination-95b63e16152e
2023-09-16 12:57:34 +02:00
Hosted Weblate
31e3509369 Translated using Weblate (Portuguese)
Currently translated at 100.0% (353 of 353 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-09-15 22:01:35 +02:00
johan12345
b03f765216 SearchSelectScreen: catch IOExceptions 2023-09-15 21:45:20 +02:00
johan12345
9222dec613 Release 1.6.9 2023-09-14 20:26:08 +02:00
johan12345
71c36fbc8f MapFragment: Change default map location behavior
Only jump back to current location on app restart if we were previously looking at the current location (and have location permission enabled). Otherwise stay at the last viewed location.

This seems to be the same as what e.g. the Google Maps app does.

#191
2023-09-14 20:16:41 +02:00
johan12345
830477e664 Automotive FilterScreen: fix possible crash when profile is deleted 2023-09-13 22:44:23 +02:00
johan12345
3ce91a9c50 ChargerDetailScreen: fix nullability issue 2023-09-10 12:39:19 +02:00
johan12345
a3b2b94b25 ACRA: switch -normal build variants to use HTTP sending as well 2023-09-06 21:51:50 +02:00
johan12345
a7770e1c1b fix Github Actions build 2023-09-03 18:34:24 +02:00
johan12345
fcd51307cb Release 1.6.8 2023-09-03 18:11:57 +02:00
johan12345
ba4a9c29b2 ACRA: keep key-value format for email reports 2023-09-03 18:11:24 +02:00
johan12345
3463177ad2 EnBW: avoid endless loop introduced by d636cde7 2023-09-02 23:25:42 +02:00
johan12345
09deaf5080 AA/AAOS: add general info & amenities
(if room available)
2023-09-02 22:27:45 +02:00
johan12345
23f429bbea disable CarAppTest due to Robolectric incompatibility 2023-09-02 22:16:35 +02:00
johan12345
1184d3b6cc AA/AAOS FilterScreen: add delete button to rows 2023-09-02 22:09:07 +02:00
johan12345
c95a60807b Upgrade SDK to 34 & dependencies 2023-09-02 22:01:47 +02:00
johan12345
4b8cf82843 update Android Gradle plugin 2023-09-02 21:20:32 +02:00
johan12345
f33b9e8117 Merge branch 'rework-acra' 2023-09-02 17:39:46 +02:00
johan12345
cbc3040807 Rework units configuration
units (imperial or metric) can be configured globally, this applies to distances within the app, vehicle data, and the map scale bar

fixes #293
2023-09-02 17:39:37 +02:00
johan12345
92619ea95e ACRA: switch from email to HTTP sending for automotive flavor 2023-09-02 16:47:56 +02:00
johan12345
a7007284ff ACRA: introduce AAOS-friendly crash reporting screen 2023-09-02 16:38:02 +02:00
johan12345
7fce566052 add referral link to donate page 2023-08-27 19:53:32 +02:00
Johan von Forstner
0c44b4b074 Update FUNDING.yml 2023-08-27 19:52:48 +02:00
johan12345
a652d96f74 fix CarAppTest 2023-08-27 19:12:55 +02:00
johan12345
8a9b3ad948 Android Auto/Automotive: Make users explicitly accept the privacy policy 2023-08-27 19:05:04 +02:00
Johan von Forstner
c48f33e265 Release 1.6.7 2023-08-21 19:26:30 +02:00
johan12345
8ba4897026 Android Automotive: update donate link 2023-08-21 19:22:45 +02:00
Johan von Forstner
42916d71ca Onboarding: Do not select GoingElectric by default
instead make the user actively choose the data source
#291
2023-08-03 16:47:20 +02:00
Johan von Forstner
5ca7524e8b OpenChargeMap: Map "Tesla Model S/X" plug to Chargepoint.SUPERCHARGER
#6
2023-08-03 16:34:10 +02:00
Johan von Forstner
c37f72a26b reduce vertical padding around dialogs
#292
2023-08-02 20:46:16 +02:00
Johan von Forstner
6f0113c50d adjustments to data source selection screen for smaller screens 2023-08-02 19:58:05 +02:00
Johan von Forstner
f99ea7ca9e Fix formatting of Portuguese string 2023-08-02 16:21:02 +02:00
Johan von Forstner
788db0c10f fix popup menu theme in dark mode
see #287
2023-08-02 16:09:48 +02:00
Hosted Weblate
db2213a50f Translated using Weblate (Portuguese)
Currently translated at 100.0% (349 of 349 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-08-02 16:08:45 +02:00
Johan von Forstner
ace4126035 more uppercase consistency 2023-08-02 15:45:47 +02:00
Johan von Forstner
d5d6e4f314 GoingElectric: nullability fix 2023-08-01 20:23:54 +02:00
Johan von Forstner
55999d15e6 upper/lower case consistency
as mentioned in comment in #287
2023-07-30 20:01:11 +02:00
Johan von Forstner
b61ca609d3 GoingElectric: fix "Tesla Supercharger CCS" filter
"Tesla Supercharger CCS" is a bit peculiar - it is available as a filter, but the API just returns "CCS" in the charging station details. So we cannot use it for filtering as it won't work in the local database. So we join them into a single filter option. If you want to find Tesla Superchargers with CCS, you can still do that using the network filter.
2023-07-27 10:16:22 +02:00
johan12345
b0afad2144 fix nullability issue in TeslaAvailabilityDetector 2023-07-19 20:59:47 +02:00
98 changed files with 1949 additions and 881 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
github: johan12345
custom: 'https://paypal.me/johan98'
custom: ['https://paypal.me/johan98', 'http://ts.la/johan94494']

View File

@@ -5,4 +5,5 @@
<string name="chargeprice_key" translatable="false">ci</string>
<string name="openchargemap_key" translatable="false">ci</string>
<string name="fronyx_key" translatable="false">ci</string>
<string name="acra_credentials" translatable="false">ci:ci</string>
</resources>

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
@@ -22,7 +22,7 @@ jobs:
- name: Decrypt keystore
run: openssl aes-256-cbc -K ${{ secrets.encrypted_53968681344a_key }} -iv ${{ secrets.encrypted_53968681344a_iv }} -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
- name: Extract version code
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s\+[0-9]*" app/build.gradle | awk '{ print $2 }' | tr -d \''"\\')" >> $GITHUB_ENV
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
- name: Build app release
env:
@@ -32,6 +32,7 @@ jobs:
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}

View File

@@ -16,10 +16,10 @@ jobs:
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'

View File

@@ -1,293 +0,0 @@
plugins {
id 'com.adarshr.test-logger' version '3.1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'pt.jcosta.resourceplaceholders'
def supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
android {
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 192
versionName "1.6.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(',')
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
}
signingConfigs {
release {
def isRunningOnCI = System.getenv("CI") == "true"
if (isRunningOnCI) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEYSTORE_ALIAS")
keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD")
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
applicationIdSuffix ".debug"
debuggable true
}
}
flavorDimensions "dependencies", "automotive"
productFlavors {
foss {
dimension "dependencies"
}
google {
dimension "dependencies"
versionNameSuffix "-google"
}
normal {
dimension "automotive"
}
automotive {
dimension "automotive"
versionNameSuffix "-automotive"
versionCode defaultConfig.versionCode + 1
minSdkVersion 29
}
}
compileOptions {
coreLibraryDesugaringEnabled true
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs).configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures {
dataBinding = true
viewBinding true
}
lint {
disable 'NullSafeMutableLiveData'
warning 'MissingTranslation'
}
testOptions {
unitTests.includeAndroidResources true
}
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all { variant ->
ext.env = System.getenv()
def goingelectricKey = env.GOINGELECTRIC_API_KEY ?: project.findProperty("GOINGELECTRIC_API_KEY")
if (goingelectricKey != null) {
variant.resValue "string", "goingelectric_key", goingelectricKey
}
def openchargemapKey = env.OPENCHARGEMAP_API_KEY ?: project.findProperty("OPENCHARGEMAP_API_KEY")
if (openchargemapKey == null && project.hasProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED")) {
openchargemapKey = decode(project.findProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (openchargemapKey != null) {
variant.resValue "string", "openchargemap_key", openchargemapKey
}
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
if (googleMapsKey != null && variant.flavorName.startsWith('google')) {
variant.resValue "string", "google_maps_key", googleMapsKey
}
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
if (mapboxKey == null && project.hasProperty("MAPBOX_API_KEY_ENCRYPTED")) {
mapboxKey = decode(project.findProperty("MAPBOX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (mapboxKey != null) {
variant.resValue "string", "mapbox_key", mapboxKey
}
def chargepriceKey = env.CHARGEPRICE_API_KEY ?: project.findProperty("CHARGEPRICE_API_KEY")
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
chargepriceKey = decode(project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (chargepriceKey != null) {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (fronyxKey != null) {
variant.resValue "string", "fronyx_key", fronyxKey
}
}
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
}
configurations {
googleNormalImplementation {}
googleAutomotiveImplementation {}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.1'
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.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
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 "androidx.work:work-runtime-ktx:2.8.1"
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.11.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.11.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
implementation 'com.squareup.moshi:moshi-adapters:1.15.0'
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
implementation 'io.coil-kt:coil:2.4.0'
implementation 'com.github.ev-map:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:4.1.0'
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
def carAppVersion = '1.3.0-rc01'
implementation "androidx.car.app:app:$carAppVersion"
normalImplementation "androidx.car.app:app-projected:$carAppVersion"
automotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
// AnyMaps
def anyMapsVersion = '8f1226e1c5'
implementation "com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
exclude group: 'com.google.android.gms', module: 'play-services-location'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-core'
}
// original version of mapbox-android-core
googleImplementation 'com.mapbox.mapboxsdk:mapbox-android-core:2.0.1'
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
// Google Places
googleImplementation 'com.google.android.libraries.places:places:3.1.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
// navigation library
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
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.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'com.github.anboralabs:spatia-room:0.2.7'
// billing library
def billing_version = "6.0.0"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
// ACRA (crash reporting)
def acraVersion = "5.8.4"
implementation("ch.acra:acra-mail:$acraVersion")
implementation("ch.acra:acra-dialog:$acraVersion")
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
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.11.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'
testImplementation 'org.robolectric:robolectric:4.9.2'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
// testing for car app
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
testGoogleImplementation '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'
androidTestImplementation 'androidx.arch.core:core-testing:2.2.0'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.15.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}
private static String decode(String s, String key) {
return new String(xorWithKey(s.decodeBase64(), key.getBytes()), "UTF-8");
}
private static byte[] xorWithKey(byte[] a, byte[] key) {
byte[] out = new byte[a.length];
for (int i = 0; i < a.length; i++) {
out[i] = (byte) (a[i] ^ key[i%key.length]);
}
return out;
}

353
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,353 @@
import java.util.Base64
plugins {
id("com.adarshr.test-logger") version "3.1.0"
id("com.android.application")
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
id("pt.jcosta.resourceplaceholders")
}
val supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
android {
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 34
minSdk = 21
targetSdk = 34
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 206
versionName = "1.7.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += supportedLocales.split(",")
buildConfigField("String", "supportedLocales", "\"$supportedLocales\"")
}
signingConfigs {
create("release") {
val isRunningOnCI = System.getenv("CI") == "true"
if (isRunningOnCI) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEYSTORE_ALIAS")
keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}
debug {
applicationIdSuffix = ".debug"
isDebuggable = true
}
}
flavorDimensions += listOf("dependencies", "automotive")
productFlavors {
create("foss") {
dimension = "dependencies"
}
create("google") {
dimension = "dependencies"
versionNameSuffix = "-google"
}
create("normal") {
dimension = "automotive"
}
create("automotive") {
dimension = "automotive"
versionNameSuffix = "-automotive"
versionCode = defaultConfig.versionCode!! + 1
minSdk = 29
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures {
dataBinding = true
viewBinding = true
}
lint {
disable += listOf("NullSafeMutableLiveData")
warning += listOf("MissingTranslation")
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
resourcePlaceholders {
files("xml/shortcuts.xml")
}
namespace = "net.vonforst.evmap"
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all {
val goingelectricKey =
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
?.toString()
if (goingelectricKey != null) {
resValue("string", "goingelectric_key", goingelectricKey)
}
var openchargemapKey =
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
?.toString()
if (openchargemapKey == null && project.hasProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED")) {
openchargemapKey = decode(
project.findProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (openchargemapKey != null) {
resValue("string", "openchargemap_key", openchargemapKey)
}
val googleMapsKey =
System.getenv("GOOGLE_MAPS_API_KEY") ?: project.findProperty("GOOGLE_MAPS_API_KEY")
?.toString()
if (googleMapsKey != null && flavorName.startsWith("google")) {
resValue("string", "google_maps_key", googleMapsKey)
}
var mapboxKey =
System.getenv("MAPBOX_API_KEY") ?: project.findProperty("MAPBOX_API_KEY")?.toString()
if (mapboxKey == null && project.hasProperty("MAPBOX_API_KEY_ENCRYPTED")) {
mapboxKey = decode(
project.findProperty("MAPBOX_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (mapboxKey != null) {
resValue("string", "mapbox_key", mapboxKey)
}
var chargepriceKey =
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
?.toString()
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
chargepriceKey = decode(
project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (chargepriceKey != null) {
resValue("string", "chargeprice_key", chargepriceKey)
}
var fronyxKey =
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
fronyxKey = decode(
project.findProperty("FRONYX_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (fronyxKey != null) {
resValue("string", "fronyx_key", fronyxKey)
}
var acraKey = System.getenv("ACRA_CRASHREPORT_CREDENTIALS")
?: project.findProperty("ACRA_CRASHREPORT_CREDENTIALS")?.toString()
if (acraKey == null && project.hasProperty("ACRA_CRASHREPORT_CREDENTIALS_ENCRYPTED")) {
acraKey = decode(
project.findProperty("ACRA_CRASHREPORT_CREDENTIALS_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (acraKey != null) {
resValue("string", "acra_credentials", acraKey)
}
}
packaging {
jniLibs {
pickFirsts.addAll(
listOf(
"lib/x86/libc++_shared.so",
"lib/arm64-v8a/libc++_shared.so",
"lib/x86_64/libc++_shared.so",
"lib/armeabi-v7a/libc++_shared.so"
)
)
}
}
}
configurations {
create("googleNormalImplementation") {}
create("googleAutomotiveImplementation") {}
}
aboutLibraries {
allowedLicenses = arrayOf(
"Apache-2.0", "mit", "BSD-2-Clause",
"asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service" // Google Maps SDK
)
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
dependencies {
val kotlinVersion: String by rootProject.extra
val aboutLibsVersion: String by rootProject.extra
val navVersion: String by rootProject.extra
val normalImplementation by configurations
val googleImplementation by configurations
val automotiveImplementation by configurations
val fossImplementation by configurations
val testGoogleImplementation by configurations
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.6.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("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.11.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.4.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
implementation("com.airbnb.android:lottie:4.1.0")
implementation("io.michaelrocks.bimap:bimap:1.1.0")
implementation("com.google.guava:guava:29.0-android")
implementation("com.github.pengrad:mapscaleview:1.6.0")
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
// Android Auto
val carAppVersion = "1.4.0-rc01"
implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "8f1226e1c5"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
exclude(group = "com.google.android.gms", module = "play-services-location")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-core")
}
// original version of mapbox-android-core
googleImplementation("com.mapbox.mapboxsdk:mapbox-android-core:2.0.1")
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation("com.github.ev-map:mapbox-events-android:a21c324501")
// Google Places
googleImplementation("com.google.android.libraries.places:places:3.2.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
// Mapbox Geocoding
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
// navigation library
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
// viewmodel library
val lifecycle_version = "2.6.2"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// room library
val room_version = "2.6.0"
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.2.7")
// billing library
val billing_version = "6.0.1"
googleImplementation("com.android.billingclient:billing:$billing_version")
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
// ACRA (crash reporting)
val acraVersion = "5.11.1"
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-dialog:$acraVersion")
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
debugImplementation("com.facebook.flipper:flipper:0.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.11.0")
//noinspection GradleDependency
testImplementation("org.json:json:20080701")
testImplementation("org.robolectric:robolectric:4.10.3")
testImplementation("androidx.test:core:1.5.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
// testing for car app
testGoogleImplementation("androidx.car.app:app-testing:$carAppVersion")
testGoogleImplementation("androidx.test:core:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
fun decode(s: String, key: String): String {
return String(xorWithKey(Base64.getDecoder().decode(s), key.toByteArray()), Charsets.UTF_8)
}
fun xorWithKey(a: ByteArray, key: ByteArray): ByteArray {
val out = ByteArray(a.size)
for (i in a.indices) {
out[i] = (a[i].toInt() xor key[i % key.size].toInt()).toByte()
}
return out
}

View File

@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View File

@@ -3,10 +3,12 @@ package net.vonforst.evmap
import android.app.Activity
import android.content.Context
@Suppress("UNUSED_PARAMETER")
fun init(context: Context) {
}
@Suppress("UNUSED_PARAMETER")
fun checkPlayServices(activity: Activity): Boolean {
return true
}

View File

@@ -42,5 +42,9 @@ class DonateFragment : Fragment() {
binding.btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
}
binding.referrals.referralTesla.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link))
}
}
}

View File

@@ -1,18 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:fitsSystemWindows="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
@@ -21,31 +19,55 @@
</com.google.android.material.appbar.AppBarLayout>
<Button
android:id="@+id/btnDonate"
style="@style/Widget.Material3.Button.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/donate_paypal"
app:icon="@drawable/ic_paypal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView20" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView20"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/donations_info"
app:layout_constraintBottom_toTopOf="@+id/btnDonate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:id="@+id/btnDonate"
style="@style/Widget.Material3.Button.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:text="@string/donate_paypal"
app:icon="@drawable/ic_paypal"
app:layout_constraintBottom_toTopOf="@id/referrals"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView20" />
<TextView
android:id="@+id/textView20"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/donations_info"
app:layout_constraintBottom_toTopOf="@+id/btnDonate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<include
android:id="@+id/referrals"
layout="@layout/fragment_donate_referral"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnDonate" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</LinearLayout>

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
@@ -18,12 +19,17 @@ import com.google.android.material.transition.MaterialSharedAxis
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DonationAdapter
import net.vonforst.evmap.adapter.SingleViewAdapter
import net.vonforst.evmap.databinding.FragmentDonateBinding
import net.vonforst.evmap.databinding.FragmentDonateHeaderBinding
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
import net.vonforst.evmap.viewmodel.DonateViewModel
class DonateFragment : Fragment() {
private lateinit var binding: FragmentDonateBinding
private val vm: DonateViewModel by viewModels()
private lateinit var header: FragmentDonateHeaderBinding
private lateinit var referrals: FragmentDonateReferralBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -40,6 +46,9 @@ class DonateFragment : Fragment() {
binding.lifecycleOwner = this
binding.vm = vm
header = FragmentDonateHeaderBinding.inflate(inflater, container, false)
referrals = FragmentDonateReferralBinding.inflate(inflater, container, false)
return binding.root
}
@@ -51,25 +60,35 @@ class DonateFragment : Fragment() {
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.productsList.apply {
adapter = DonationAdapter().apply {
onClickListener = {
vm.startPurchase(it, requireActivity())
}
val donationAdapter = DonationAdapter().apply {
onClickListener = {
vm.startPurchase(it, requireActivity())
}
}
binding.productsList.apply {
val joinedAdapter = ConcatAdapter(
SingleViewAdapter(header.root),
donationAdapter,
SingleViewAdapter(referrals.root)
)
adapter = joinedAdapter
layoutManager = LinearLayoutManager(context)
}
vm.products.observe(viewLifecycleOwner) {
print(it)
donationAdapter.submitList(it.data)
}
vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer {
vm.purchaseSuccessful.observe(viewLifecycleOwner) {
Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show()
})
vm.purchaseFailed.observe(viewLifecycleOwner, Observer {
}
vm.purchaseFailed.observe(viewLifecycleOwner) {
Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show()
})
}
referrals.referralTesla.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.tesla_referral_link))
}
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))

View File

@@ -35,29 +35,16 @@
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/textView20"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/donations_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/products_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:data="@{vm.products.data}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView20"
tools:listitem="@layout/item_donation" />
app:layout_constraintTop_toBottomOf="@id/toolbar_container"
tools:itemCount="1"
tools:listitem="@layout/fragment_donate_preview" />
<ProgressBar
android:id="@+id/progressBar3"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/textView20"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/donations_info"
xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/fragment_donate_header" />
<include layout="@layout/item_donation" />
<include layout="@layout/item_donation" />
<include layout="@layout/item_donation" />
<include layout="@layout/fragment_donate_referral" />
</LinearLayout>

View File

@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
@@ -289,6 +290,10 @@
android:resource="@xml/shortcuts" />
</activity>
<activity android:name=".auto.OAuthLoginActivity">
</activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
@@ -327,7 +332,8 @@
android:name=".auto.CarAppService"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:exported="true">
android:exported="true"
android:foregroundServiceType="location">
<intent-filter>
<action
android:name="androidx.car.app.CarAppService"

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap
import android.app.Activity
import android.app.Application
import android.os.Build
import androidx.work.*
@@ -8,10 +9,11 @@ import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode
import org.acra.config.dialog
import org.acra.config.httpSender
import org.acra.config.limiter
import org.acra.config.mailSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import java.time.Duration
class EvMapApplication : Application(), Configuration.Provider {
@@ -33,10 +35,15 @@ class EvMapApplication : Application(), Configuration.Provider {
if (!BuildConfig.DEBUG) {
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.KEY_VALUE_LIST
mailSender {
mailTo = "evmap+crashreport@vonforst.net"
// Vehicles often don't have an email app, so use HTTP to send instead
reportFormat = StringFormat.JSON
httpSender {
uri = getString(R.string.acra_backend_url)
val creds = getString(R.string.acra_credentials).split(":")
basicAuthLogin = creds[0]
basicAuthPassword = creds[1]
httpMethod = HttpSender.Method.POST
}
dialog {
@@ -45,6 +52,10 @@ class EvMapApplication : Application(), Configuration.Provider {
commentPrompt = getString(R.string.crash_report_comment_prompt)
resIcon = R.drawable.ic_launcher_foreground
resTheme = R.style.AppTheme
if (BuildConfig.FLAVOR_automotive == "automotive") {
reportDialogClass =
Class.forName("androidx.car.app.activity.CarAppActivity") as Class<out Activity>?
}
}
limiter {
@@ -61,7 +72,7 @@ class EvMapApplication : Application(), Configuration.Provider {
}
}.build()).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.REPLACE, cleanupCacheRequest
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
)
}

View File

@@ -5,10 +5,13 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Typeface
import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.os.Build
import android.os.Bundle
import android.text.*
import android.text.style.StyleSpan
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.*
fun Bundle.optDouble(name: String): Double? {
@@ -88,9 +91,25 @@ fun Context.isDarkMode() =
const val kmPerMile = 1.609344
const val meterPerFt = 0.3048
const val ftPerMile = 5280
const val ydPerMile = 1760
fun shouldUseImperialUnits(ctx: Context): Boolean {
val prefs = PreferenceDataSource(ctx)
return when (prefs.units) {
"metric" -> false
"imperial" -> true
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
when (LocaleData.getMeasurementSystem(ULocale.getDefault())) {
LocaleData.MeasurementSystem.US, LocaleData.MeasurementSystem.UK -> true
LocaleData.MeasurementSystem.SI -> false
else -> false
}
} else {
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
}
}
fun shouldUseImperialUnits(): Boolean {
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
}
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =

View File

@@ -30,6 +30,7 @@ abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
var onClickListener: ((T) -> Unit)? = null
var onLongClickListener: ((T) -> Boolean)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<T> {
val layoutInflater = LayoutInflater.from(parent.context)
@@ -54,6 +55,12 @@ abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
listener(item)
}
}
if (onLongClickListener != null) {
holder.binding.root.setOnLongClickListener {
val listener = onLongClickListener ?: return@setOnLongClickListener false
return@setOnLongClickListener listener(item)
}
}
}
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
@@ -209,8 +216,8 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
checkedItem = item
root.post {
notifyDataSetChanged()
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
}
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
}
}
}

View File

@@ -139,7 +139,7 @@ fun buildDetails(
)
}
private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
ctx.getString(
R.string.tesla_pricing_blocking_fee,
@@ -147,7 +147,7 @@ private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Co
)
}
private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
buildSpannedString {
teslaPricing.memberRates?.let { memberRates ->
append(

View File

@@ -170,9 +170,11 @@ class AvailabilityRepository(context: Context) {
.connectTimeout(10, TimeUnit.SECONDS)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
private val teslaAvailabilityDetector =
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context))
private val availabilityDetectors = listOf(
RheinenergieAvailabilityDetector(okhttp),
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)),
teslaAvailabilityDetector,
EnBwAvailabilityDetector(okhttp),
NewMotionAvailabilityDetector(okhttp)
)
@@ -199,4 +201,10 @@ class AvailabilityRepository(context: Context) {
}
return value ?: Resource.error(null, null)
}
fun isSupercharger(charger: ChargeLocation) =
teslaAvailabilityDetector.isChargerSupported(charger)
fun isTeslaSupported(charger: ChargeLocation) =
teslaAvailabilityDetector.isChargerSupported(charger) && teslaAvailabilityDetector.isSignedIn()
}

View File

@@ -105,20 +105,19 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
var markers =
api.getMarkers(lng - coordRange, lng + coordRange, lat - coordRange, lat + coordRange)
while (markers.any { it.grouped }) {
markers = markers.flatMap {
if (it.grouped) {
api.getMarkers(
it.viewPort.lowerLeftLon,
it.viewPort.upperRightLon,
it.viewPort.lowerLeftLat,
it.viewPort.upperRightLat
)
} else {
listOf(it)
}
markers = markers.flatMap {
if (it.grouped) {
api.getMarkers(
it.viewPort.lowerLeftLon,
it.viewPort.upperRightLon,
it.viewPort.lowerLeftLat,
it.viewPort.upperRightLat
)
} else {
listOf(it)
}
}
if (markers.any { it.grouped }) throw AvailabilityDetectorException("markers still grouped")
val nearest = markers.minByOrNull { marker ->
distanceBetween(marker.lat, marker.lon, lat, lng)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.api.availability
import android.net.Uri
import android.util.Base64
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
@@ -102,6 +103,18 @@ interface TeslaAuthenticationApi {
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}
fun buildSignInUri(codeChallenge: String): 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 resultUrlPrefix = "https://auth.tesla.com/void/callback"
}
}
@@ -235,7 +248,7 @@ interface TeslaGraphQlApi {
data class GetNearbyChargingSitesResponse(val data: GetNearbyChargingSitesResponseData)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging)
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging?)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponseDataCharging(val nearbySites: GetNearbyChargingSitesResponseDataChargingNearbySites)
@@ -274,7 +287,7 @@ interface TeslaGraphQlApi {
data class GetChargingSiteInformationResponseData(val charging: GetChargingSiteInformationResponseDataCharging)
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation)
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation?)
@JsonClass(generateAdapter = true)
data class ChargingSiteInformation(
@@ -303,13 +316,13 @@ interface TeslaGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargerId(
val id: Text,
val label: Value<String>,
val label: Value<String>?,
val name: String?
) {
val labelNumber
get() = label.value.replace(Regex("""\D"""), "").toInt()
get() = label?.value?.replace(Regex("""\D"""), "")?.toInt()
val labelLetter
get() = label.value.replace(Regex("""\d"""), "")
get() = label?.value?.replace(Regex("""\d"""), "")
}
@JsonClass(generateAdapter = true)
@@ -432,6 +445,9 @@ interface TeslaGraphQlApi {
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_20_MINUTES")
APPROXIMATELY_20_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_GREATER_THAN_25_MINUTES")
GREATER_THAN_25_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_UNKNOWN")
UNKNOWN
}
@@ -518,7 +534,8 @@ class TeslaAvailabilityDetector(
val results = api.getNearbyChargingSites(
req,
req.operationName
).data.charging.nearbySites.sitesAndDistances
).data.charging?.nearbySites?.sitesAndDistances
?: throw AvailabilityDetectorException("no candidates found.")
val result =
results.minByOrNull { it.haversineDistanceMiles.value }
?: throw AvailabilityDetectorException("no candidates found.")
@@ -530,7 +547,7 @@ class TeslaAvailabilityDetector(
TeslaGraphQlApi.VehicleMakeType.NON_TESLA
)
)
).data.charging.site
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
@@ -553,8 +570,16 @@ class TeslaAvailabilityDetector(
"charger has unknown connectors"
)
var statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
.sortedBy { it.charger.labelNumber }.map { it.availability }
var statusSorted = details.siteDynamic.chargerDetails
.sortedBy { c ->
c.charger.labelLetter
?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelLetter
}
.sortedBy { c ->
c.charger.labelNumber
?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelNumber
}
.map { it.availability }
if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
// apparently some connectors are missing in Tesla data
// If we have just one type of charger, we can still match
@@ -641,4 +666,6 @@ class TeslaAvailabilityDetector(
}
}
fun isSignedIn() = tokenStore.teslaRefreshToken != null
}

View File

@@ -0,0 +1,188 @@
package net.vonforst.evmap.api.fronyx
import android.content.Context
import com.squareup.moshi.JsonDataException
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.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.Resource
import retrofit2.HttpException
import java.io.IOException
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
data class PredictionData(
val predictionGraph: Map<ZonedDateTime, Double>?,
val maxValue: Double,
val predictedChargepoints: List<Chargepoint>,
val isPercentage: Boolean,
val description: String?
)
class PredictionRepository(private val context: Context) {
private val predictionApi = FronyxApi(context.getString(R.string.fronyx_key))
private val prefs = PreferenceDataSource(context)
suspend fun getPredictionData(
charger: ChargeLocation,
availability: ChargeLocationStatus?,
filteredConnectors: Set<String>? = null
): PredictionData {
val fronyxPrediction = availability?.evseIds?.let { evseIds ->
getFronyxPrediction(charger, evseIds, filteredConnectors)
}?.data
val graph = buildPredictionGraph(availability, fronyxPrediction)
val predictedChargepoints = getPredictedChargepoints(charger, filteredConnectors)
val maxValue = getPredictionMaxValue(availability, fronyxPrediction, predictedChargepoints)
val isPercentage = predictionIsPercentage(availability, fronyxPrediction)
val description = getDescription(charger, predictedChargepoints)
return PredictionData(
graph, maxValue, predictedChargepoints, isPercentage, description
)
}
private suspend fun getFronyxPrediction(
charger: ChargeLocation,
evseIds: Map<Chargepoint, List<String>>,
filteredConnectors: Set<String>?
): Resource<List<FronyxEvseIdResponse>> {
if (!prefs.predictionEnabled) return Resource.success(null)
val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors?.let { filtered ->
equivalentPlugTypes(
it.type
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
if (allEvseIds.isEmpty()) {
return Resource.success(emptyList())
}
try {
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
if (result.size == allEvseIds.size) {
return Resource.success(result)
} else {
return Resource.error("not all EVSEIDs found", null)
}
} catch (e: IOException) {
e.printStackTrace()
return Resource.error(e.message, null)
} catch (e: HttpException) {
e.printStackTrace()
return Resource.error(e.message, null)
} catch (e: AvailabilityDetectorException) {
e.printStackTrace()
return Resource.error(e.message, null)
} catch (e: JsonDataException) {
// malformed JSON response from fronyx API
e.printStackTrace()
return Resource.error(e.message, null)
}
}
private fun buildPredictionGraph(
availability: ChargeLocationStatus?,
prediction: List<FronyxEvseIdResponse>?
): Map<ZonedDateTime, Double>? {
val congestionHistogram = availability?.congestionHistogram
return 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 }
}
}
}
}
private fun getPredictedChargepoints(
charger: ChargeLocation,
filteredConnectors: Set<String>?
) =
charger.chargepoints.filter {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors?.let { filtered ->
equivalentPlugTypes(it.type).any {
filtered.contains(
it
)
}
} ?: true
}
private fun getPredictionMaxValue(
availability: ChargeLocationStatus?,
prediction: List<FronyxEvseIdResponse>?,
predictedChargepoints: List<Chargepoint>
): Double = if (availability?.congestionHistogram != null && prediction == null) {
1.0
} else {
predictedChargepoints.sumOf { it.count }.toDouble()
}
private fun predictionIsPercentage(
availability: ChargeLocationStatus?,
prediction: List<FronyxEvseIdResponse>?
) =
availability?.congestionHistogram != null && prediction == null
private fun getDescription(
charger: ChargeLocation,
predictedChargepoints: List<Chargepoint>
): String? {
val allChargepoints = charger.chargepoints
val predictedChargepointTypes = predictedChargepoints.map { it.type }.distinct()
return if (allChargepoints == predictedChargepoints) {
null
} else if (predictedChargepointTypes.size == 1) {
context.getString(
R.string.prediction_only,
nameForPlugType(context.stringProvider(), predictedChargepointTypes[0])
)
} else {
context.getString(
R.string.prediction_only,
context.getString(R.string.prediction_dc_plugs_only)
)
}
}
}

View File

@@ -153,6 +153,10 @@ class GoingElectricApiWrapper(
// no connectors chosen
return Resource.success(ChargepointList.empty())
}
if (connectorsVal != null && connectorsVal.values.contains("CCS")) {
// see note about Tesla Supercharger CCS filter in getFilters below
connectorsVal.values.add("Tesla Supercharger CCS")
}
val connectors = formatMultipleChoice(connectorsVal)
val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards")
@@ -247,6 +251,10 @@ class GoingElectricApiWrapper(
// no connectors chosen
return Resource.success(ChargepointList.empty())
}
if (connectorsVal != null && connectorsVal.values.contains("CCS")) {
// see note about Tesla Supercharger CCS filter in getFilters below
connectorsVal.values.add("Tesla Supercharger CCS")
}
val connectors = formatMultipleChoice(connectorsVal)
val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards")
@@ -434,9 +442,18 @@ class GoingElectricApiWrapper(
val networks = refData.networks
val chargeCards = refData.chargecards
val plugMap = plugs.associateWith { plug ->
nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
}
/*
"Tesla Supercharger CCS" is a bit peculiar - it is available as a filter, but the API
just returns "CCS" in the charging station details. So we cannot use it for filtering as
it won't work in the local database. So we join them into a single filter option.
If you want to find Tesla Superchargers with CCS, you can still do that using the network
filter.
*/
val plugMap = plugs
.filter { it != "Tesla Supercharger CCS" }
.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(

View File

@@ -5,7 +5,22 @@ import androidx.room.PrimaryKey
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.Address
import net.vonforst.evmap.model.ChargeCard
import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ChargeLocationCluster
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.ChargepriceData
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.Hours
import net.vonforst.evmap.model.OpeningHours
import net.vonforst.evmap.model.OpeningHoursDays
import net.vonforst.evmap.model.ReferenceData
import java.time.Instant
import java.time.LocalTime
@@ -35,7 +50,7 @@ sealed class GEChargepointListItem {
@JsonClass(generateAdapter = true)
data class GEChargeLocation(
@Json(name = "ge_id") val id: Long,
val name: String,
val name: String?,
val coordinates: GECoordinate,
val address: GEAddress,
val chargepoints: List<GEChargepoint>,
@@ -57,7 +72,7 @@ data class GEChargeLocation(
override fun convert(apikey: String, isDetailed: Boolean) = ChargeLocation(
id,
"goingelectric",
name,
name ?: "Charging station",
coordinates.convert(),
address.convert(),
chargepoints.map { it.convert() },

View File

@@ -6,7 +6,15 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.max
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.Address
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargepriceData
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.ReferenceData
import java.time.Instant
import java.time.ZonedDateTime
@@ -56,8 +64,8 @@ data class OCMChargepoint(
addressInfo.toAddress(refData),
connections.map { it.convert(refData) },
operatorInfo?.title,
"https://openchargemap.org/site/poi/details/$id",
"https://openchargemap.org/site/poi/edit/$id",
"https://map.openchargemap.io/?id=$id",
"https://map.openchargemap.io/?id=$id",
convertFaultReport(),
recentlyVerified,
null,
@@ -165,7 +173,8 @@ data class OCMConnection(
17L -> Chargepoint.CEE_ROT
28L -> Chargepoint.SCHUKO
8L -> Chargepoint.TESLA_ROADSTER_HPC
27L -> Chargepoint.SUPERCHARGER
27L -> Chargepoint.SUPERCHARGER // Tesla North American plug (NACS)
30L -> Chargepoint.SUPERCHARGER // European Tesla Model S/X Supercharger plug (DC on Type 2)
25L -> Chargepoint.TYPE_2_SOCKET
1036L -> Chargepoint.TYPE_2_PLUG
1L -> Chargepoint.TYPE_1

View File

@@ -34,6 +34,7 @@ import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.checkFineLocationPermission
import org.acra.interaction.DialogInteraction
interface LocationAwareScreen {
@@ -44,13 +45,15 @@ interface LocationAwareScreen {
class CarAppService : androidx.car.app.CarAppService() {
private val CHANNEL_ID = "car_location"
private val NOTIFICATION_ID = 1000
private var foregroundStarted = false
override fun onCreate() {
super.onCreate()
fun ensureForegroundService() {
// we want to run as a foreground service to make sure we can use location
createNotificationChannel()
startForeground(NOTIFICATION_ID, getNotification())
if (!foregroundStarted) {
createNotificationChannel()
startForeground(NOTIFICATION_ID, getNotification())
foregroundStarted = true
}
}
private fun createNotificationChannel() {
@@ -122,11 +125,13 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}
override fun onCreateScreen(intent: Intent): Screen {
handleActionsIntent(intent)
val mapScreen = MapScreen(carContext, this)
val screens = mutableListOf<Screen>(mapScreen)
handleActionsIntent(intent)?.let {
screens.add(it)
}
if (!prefs.dataSourceSet) {
screens.add(
ChooseDataSourceScreen(
@@ -149,6 +154,14 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
)
)
}
if (!prefs.privacyAccepted) {
screens.add(
AcceptPrivacyScreen(carContext)
)
}
handleACRAIntent(intent)?.let {
screens.add(it)
}
if (screens.size > 1) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
@@ -160,7 +173,13 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
return screens.last()
}
private fun handleActionsIntent(intent: Intent): Boolean {
private fun handleACRAIntent(intent: Intent): Screen? {
return if (intent.hasExtra(DialogInteraction.EXTRA_REPORT_CONFIG)) {
CrashReportScreen(carContext, intent)
} else null
}
private fun handleActionsIntent(intent: Intent): Screen? {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
@@ -169,15 +188,14 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
if (lat != null && lon != null) {
prefs.placeSearchResultAndroidAuto = LatLng(lat, lon)
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
return true
return null
} else if (name != null) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(PlaceSearchScreen(carContext, this, name))
return true
val screen = PlaceSearchScreen(carContext, this, name)
return screen
}
}
}
return false
return null
}
override fun onNewIntent(intent: Intent) {
@@ -206,6 +224,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
@SuppressLint("MissingPermission")
fun requestLocationUpdates() {
if (!locationPermissionGranted()) return
cas.ensureForegroundService()
Log.i(TAG, "Requesting location updates")
requestCarHardwareLocationUpdates()
requestPhoneLocationUpdates()

View File

@@ -12,6 +12,7 @@ import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.scale
import androidx.core.text.HtmlCompat
@@ -23,10 +24,16 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.adapter.formatTeslaParkingFee
import net.vonforst.evmap.adapter.formatTeslaPricing
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
@@ -45,6 +52,8 @@ import net.vonforst.evmap.viewmodel.awaitFinished
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -52,12 +61,17 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
var charger: ChargeLocation? = null
var photo: Bitmap? = null
private var availability: ChargeLocationStatus? = null
private var prediction: PredictionData? = null
private var fronyxSupported = false
private var teslaSupported = false
val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx)
private val predictionRepo = PredictionRepository(ctx)
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
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
@@ -290,9 +304,106 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}.build())
}
}
if (rows.count() < maxRows && charger.generalInformation != null) {
rows.add(Row.Builder().apply {
setTitle(carContext.getString(R.string.general_info))
addText(charger.generalInformation)
}.build())
}
if (rows.count() < maxRows && charger.amenities != null) {
rows.add(Row.Builder().apply {
setTitle(carContext.getString(R.string.amenities))
addText(charger.amenities)
}.build())
}
if (rows.count() < maxRows && ((fronyxSupported && prefs.predictionEnabled) || teslaSupported)) {
rows.add(1, Row.Builder().apply {
setTitle(
if (fronyxSupported) {
carContext.getString(R.string.utilization_prediction) + " (" + carContext.getString(
R.string.powered_by_fronyx
) + ")"
} else carContext.getString(R.string.average_utilization)
)
generatePredictionGraph()?.let { addText(it) }
?: addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading))
}.build())
}
if (rows.count() < maxRows && teslaSupported) {
val teslaPricing = availability?.extraData as? TeslaGraphQlApi.Pricing
rows.add(3, Row.Builder().apply {
setTitle(carContext.getString(R.string.cost))
teslaPricing?.let {
var text = formatTeslaPricing(teslaPricing, carContext) as CharSequence
formatTeslaParkingFee(teslaPricing, carContext)?.let { text += "\n\n" + it }
addText(text)
} ?: {
addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading))
}
}.build())
}
return rows
}
private fun generatePredictionGraph(): CharSequence? {
val predictionData = prediction ?: return null
val graphData = predictionData.predictionGraph?.toList() ?: return null
val maxValue = predictionData.maxValue
val maxWidth = if (BuildConfig.FLAVOR_automotive == "automotive") 25 else 18
val step = maxOf(graphData.size.toFloat() / maxWidth, 1f)
val values = graphData.map { it.second }
val graph = buildGraph(values, step, maxValue, predictionData.isPercentage)
val measurer = TextMeasurer(carContext)
val width = measurer.measureText(graph)
val startTime = timeFormat.format(graphData[0].first)
val endTime = timeFormat.format(graphData.last().first)
val baseWidth = measurer.measureText(startTime + endTime)
val spaceWidth = measurer.measureText(" ")
val numSpaces = floor((width - baseWidth) / spaceWidth).toInt()
val legend = startTime + " ".repeat(numSpaces) + endTime
return graph + "\n" + legend
}
private fun buildGraph(
values: List<Double>,
step: Float,
maxValue: Double,
isPercentage: Boolean
): CharSequence {
val sparklines = "▁▂▃▄▅▆▇█"
val graph = SpannableStringBuilder()
var i = 0f
while (i.roundToInt() < values.size) {
val v = values[i.roundToInt()]
val fraction = v / maxValue
val sparkline = sparklines[(fraction * (sparklines.length - 1)).roundToInt()].toString()
val color = if (isPercentage) {
when (v) {
in 0.0..0.5 -> CarColor.GREEN
in 0.5..0.8 -> CarColor.YELLOW
else -> CarColor.RED
}
} else {
if (v < maxValue) CarColor.GREEN else CarColor.RED
}
graph.append(
sparkline,
ForegroundCarColorSpan.create(color),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
i += step
}
return graph
}
private fun generateCostStatusText(cost: Cost): CharSequence {
val string = SpannableString(cost.getStatusText(carContext, emoji = true))
// replace emoji with CarIcon
@@ -371,8 +482,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
} else {
append(nameForPlugType(carContext.stringProvider(), cp.type))
}
append(" ")
append(cp.formatPower())
cp.formatPower()?.let {
append(" ")
append(it)
}
}
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
@@ -407,7 +520,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val intent =
Intent(
CarContext.ACTION_NAVIGATE,
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
Uri.parse("geo:${coord.lat},${coord.lng}")
)
carContext.startCarApp(intent)
}
@@ -463,12 +576,23 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
)
this@ChargerDetailScreen.photo = outImg
}
fronyxSupported = charger.chargepoints.any {
FronyxApi.isChargepointSupported(
charger,
it
)
} && !availabilityRepo.isSupercharger(charger)
teslaSupported = availabilityRepo.isTeslaSupported(charger)
invalidate()
availability = availabilityRepo.getAvailability(charger).data
invalidate()
prediction = predictionRepo.getPredictionData(charger, availability)
invalidate()
} else {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)

View File

@@ -0,0 +1,45 @@
package net.vonforst.evmap.auto
import android.content.Intent
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import net.vonforst.evmap.R
import org.acra.dialog.CrashReportDialogHelper
/**
* ACRA-compatible crash reporting screen for the Car App Library
*
* only used on Android Automotive OS
*/
class CrashReportScreen(ctx: CarContext, intent: Intent) : Screen(ctx) {
val helper = CrashReportDialogHelper(ctx, intent)
override fun onGetTemplate(): Template {
return MessageTemplate.Builder(carContext.getString(R.string.crash_report_text)).apply {
setHeaderAction(Action.APP_ICON)
setTitle(carContext.getString(R.string.app_name))
addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.ok))
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
helper.sendCrash(null, null)
screenManager.pop()
}.build()
)
addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.cancel))
.setOnClickListener {
helper.cancelReports()
screenManager.pop()
}.build()
)
}.build()
}
}

View File

@@ -41,7 +41,9 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
if (filterStatus in listOf(FILTERS_DISABLED, FILTERS_FAVORITES, FILTERS_CUSTOM)) {
page = 0
} else {
page = paginateProfiles(it).indexOfFirst { it.any { it.id == filterStatus } }
val index =
paginateProfiles(it).indexOfFirst { it.any { it.id == filterStatus } }
page = index.takeUnless { it == -1 } ?: 0
}
invalidate()
}
@@ -204,6 +206,37 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
Row.IMAGE_TYPE_ICON
)
setOnClickListener { onItemClick(it.id) }
if (carContext.carAppApiLevel >= 6) {
// Delete action
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_delete
)
).build()
)
setOnClickListener {
lifecycleScope.launch {
db.filterProfileDao().delete(it)
if (prefs.filterStatus == it.id) {
prefs.filterStatus = FILTERS_DISABLED
}
CarToast.makeText(
carContext,
carContext.getString(
R.string.deleted_item,
it.name
),
CarToast.LENGTH_SHORT
).show()
invalidate()
}
}
}.build())
}
}.build())
}
if (page < paginatedProfiles.size - 1) {
@@ -293,7 +326,8 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
setActionStrip(ActionStrip.Builder().apply {
val currentProfile = vm.filterProfile.value
if (currentProfile != null) {
if (currentProfile != null && carContext.carAppApiLevel < 6) {
// Delete action (when row actions are not available)
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
@@ -310,7 +344,7 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
CarToast.makeText(
carContext,
carContext.getString(
R.string.deleted_filterprofile,
R.string.deleted_item,
currentProfile.name
),
CarToast.LENGTH_SHORT

View File

@@ -348,7 +348,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
DistanceSpan.create(
roundValueToDistance(
distanceMeters,
energyLevel?.distanceDisplayUnit?.value
energyLevel?.distanceDisplayUnit?.value,
carContext
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE

View File

@@ -0,0 +1,31 @@
package net.vonforst.evmap.auto
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import net.vonforst.evmap.R
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<OAuthLoginFragment>(R.id.fragment_container_view, args = intent.extras)
}
}
LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
finish()
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
}
}

View File

@@ -23,6 +23,7 @@ class PermissionScreen(
Action.Builder()
.setTitle(carContext.getString(R.string.grant_on_phone))
.setBackgroundColor(CarColor.PRIMARY)
.setFlags(Action.FLAG_PRIMARY)
.setOnClickListener(ParkedOnlyOnClickListener.create {
requestPermissions()
})

View File

@@ -105,7 +105,8 @@ class PlaceSearchScreen(
DistanceSpan.create(
roundValueToDistance(
it,
energyLevel?.distanceDisplayUnit?.value
energyLevel?.distanceDisplayUnit?.value,
carContext
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
@@ -115,7 +116,7 @@ class PlaceSearchScreen(
setOnClickListener {
lifecycleScope.launch {
val placeDetails = getDetails(place.id)
val placeDetails = getDetails(place.id) ?: return@launch
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
prefs.placeSearchResultAndroidAutoName =
place.primaryText.toString()
@@ -225,9 +226,9 @@ class PlaceSearchScreen(
}
}
suspend fun getDetails(id: String): PlaceWithBounds {
suspend fun getDetails(id: String): PlaceWithBounds? {
val provider = currentProvider!!
val result = resultList!!.find { it.id == id }!!
val result = resultList?.find { it.id == id } ?: return null
val recentPlace = recentResults.find { it.id == id }
if (recentPlace != null) return recentPlace.asPlaceWithBounds()

View File

@@ -7,8 +7,11 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.R
import okio.IOException
abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
SearchTemplate.SearchCallback {
@@ -22,9 +25,20 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
override fun onGetTemplate(): Template {
if (fullList == null) {
lifecycleScope.launch {
fullList = loadData()
filterList()
invalidate()
try {
fullList = loadData()
filterList()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.generic_connection_error,
CarToast.LENGTH_LONG
).show()
screenManager.pop()
}
}
}
}

View File

@@ -1,10 +1,16 @@
package net.vonforst.evmap.auto
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager.NameNotFoundException
import android.hardware.Sensor
import android.hardware.SensorManager
import android.net.Uri
import android.os.Bundle
import android.os.IInterface
import android.text.Html
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
@@ -12,15 +18,27 @@ import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.core.content.IntentCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.TeslaOwnerApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.storage.PreferenceDataSource
import okhttp3.OkHttpClient
import java.io.IOException
import java.time.Instant
import kotlin.math.max
import kotlin.math.min
@@ -123,6 +141,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
val db = AppDatabase.getInstance(ctx)
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
@@ -132,6 +151,8 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val searchProviderValues =
carContext.resources.getStringArray(R.array.pref_search_provider_values)
var teslaLoggingIn = false
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.settings_data_sources))
@@ -181,9 +202,122 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
}
}
}.build())
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.pref_prediction_enabled))
.addText(carContext.getString(R.string.pref_prediction_enabled_summary))
.setToggle(Toggle.Builder {
prefs.predictionEnabled = it
}.setChecked(prefs.predictionEnabled).build())
.build()
)
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_tesla_account))
addText(
if (encryptedPrefs.teslaRefreshToken != null) {
carContext.getString(
R.string.pref_tesla_account_enabled,
encryptedPrefs.teslaEmail
)
} else if (teslaLoggingIn) {
carContext.getString(R.string.logging_in)
} else {
carContext.getString(R.string.pref_tesla_account_disabled)
}
)
if (encryptedPrefs.teslaRefreshToken != null) {
setOnClickListener {
teslaLogout()
}
} else {
setOnClickListener(ParkedOnlyOnClickListener.create {
teslaLogin()
})
}
}.build())
}.build())
}.build()
}
private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)
val args = OAuthLoginFragmentArgs(
uri.toString(),
TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
).toBundle()
val intent = Intent(carContext, OAuthLoginActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtras(args)
LocalBroadcastManager.getInstance(carContext)
.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val url = IntentCompat.getParcelableExtra(
intent,
OAuthLoginFragment.EXTRA_URL,
Uri::class.java
)
teslaGetAccessToken(url!!, codeVerifier)
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
carContext.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
}
private fun teslaGetAccessToken(url: Uri, codeVerifier: String) {
teslaLoggingIn = true
invalidate()
val code = url.getQueryParameter("code") ?: return
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) {
CarToast.makeText(
carContext,
R.string.generic_connection_error,
CarToast.LENGTH_SHORT
).show()
} finally {
teslaLoggingIn = false
}
invalidate()
}
}
private fun teslaLogout() {
// sign out
encryptedPrefs.teslaRefreshToken = null
encryptedPrefs.teslaAccessToken = null
encryptedPrefs.teslaAccessTokenExpiry = -1
encryptedPrefs.teslaEmail = null
CarToast.makeText(carContext, R.string.logged_out, CarToast.LENGTH_SHORT).show()
invalidate()
}
}
class ChooseDataSourceScreen(
@@ -646,7 +780,7 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setOnClickListener(ParkedOnlyOnClickListener.create {
if (BuildConfig.FLAVOR_automotive == "automotive") {
// we can't open the donation page on the phone in this case
openUrl(carContext, carContext.getString(R.string.paypal_link))
openUrl(carContext, carContext.getString(R.string.donate_link))
} else {
val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -703,6 +837,34 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
}
}
class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
val textWithoutLink = HtmlCompat.fromHtml(
carContext.getString(R.string.accept_privacy),
HtmlCompat.FROM_HTML_MODE_LEGACY
).toString()
return MessageTemplate.Builder(textWithoutLink).apply {
setTitle(carContext.getString(R.string.privacy))
addAction(Action.Builder()
.setTitle(carContext.getString(R.string.ok))
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
prefs.privacyAccepted = true
screenManager.pop()
}.build()
)
addAction(Action.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
}).build()
)
}.build()
}
}
class DeveloperOptionsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)

View File

@@ -4,7 +4,9 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Typeface
import android.net.Uri
import android.text.TextPaint
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext
@@ -19,7 +21,11 @@ import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.ftPerMile
import net.vonforst.evmap.getPackageInfoCompat
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.shouldUseImperialUnits
import net.vonforst.evmap.ydPerMile
import java.util.*
import kotlin.math.roundToInt
@@ -69,33 +75,26 @@ val emptyCarIcon: CarIcon by lazy {
).asCarIcon()
}
private const val kmPerMile = 1.609344
private const val ftPerMile = 5280
private const val ydPerMile = 1760
fun getDefaultDistanceUnit(): Int {
return if (usesImperialUnits(Locale.getDefault())) {
fun getDefaultDistanceUnit(ctx: Context): Int {
return if (shouldUseImperialUnits(ctx)) {
CarUnit.MILE
} else {
CarUnit.KILOMETER
}
}
fun usesImperialUnits(locale: Locale): Boolean {
return locale.country in listOf("US", "GB", "MM", "LR")
|| locale.country == "" && locale.language == "en"
}
fun getDefaultSpeedUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
else -> CarUnit.KILOMETERS_PER_HOUR
fun getDefaultSpeedUnit(ctx: Context): Int {
return if (shouldUseImperialUnits(ctx)) {
CarUnit.MILES_PER_HOUR
} else {
CarUnit.KILOMETERS_PER_HOUR
}
}
fun formatCarUnitDistance(value: Float?, unit: Int?): String {
fun formatCarUnitDistance(value: Float?, unit: Int?, ctx: Context): String {
if (value == null) return ""
return when (unit ?: getDefaultDistanceUnit()) {
return when (unit ?: getDefaultDistanceUnit(ctx)) {
// distance units: base unit is meters
CarUnit.METER -> "%.0f m".format(value)
CarUnit.KILOMETER -> "%.1f km".format(value / 1000)
@@ -105,9 +104,9 @@ fun formatCarUnitDistance(value: Float?, unit: Int?): String {
}
}
fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
fun formatCarUnitSpeed(value: Float?, unit: Int?, ctx: Context): String {
if (value == null) return ""
return when (unit ?: getDefaultSpeedUnit()) {
return when (unit ?: getDefaultSpeedUnit(ctx)) {
// speed units: base unit is meters per second
CarUnit.METERS_PER_SEC -> "%.0f m/s".format(value)
CarUnit.KILOMETERS_PER_HOUR -> "%.0f km/h".format(value * 3.6)
@@ -116,9 +115,9 @@ fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
}
}
fun roundValueToDistance(value: Double, unit: Int? = null): Distance {
fun roundValueToDistance(value: Double, unit: Int? = null, ctx: Context): Distance {
// value is in meters
when (unit ?: getDefaultDistanceUnit()) {
when (unit ?: getDefaultDistanceUnit(ctx)) {
CarUnit.MILE -> {
// imperial system
val miles = value / 1000 / kmPerMile
@@ -261,4 +260,17 @@ class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
.build()
}
}
class TextMeasurer(ctx: CarContext) {
val textPaint = TextPaint()
init {
textPaint.textSize = ctx.resources.displayMetrics.density * 24
textPaint.typeface = Typeface.DEFAULT
}
fun measureText(text: CharSequence): Float {
return textPaint.measureText(text, 0, text.length)
}
}

View File

@@ -139,7 +139,8 @@ class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
setText(
formatCarUnitDistance(
energyLevel.rangeRemainingMeters.value,
energyLevel.distanceDisplayUnit.value
energyLevel.distanceDisplayUnit.value,
carContext
)
)
setImage(
@@ -173,7 +174,8 @@ class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
setText(
formatCarUnitSpeed(
rawSpeed,
speed.speedDisplayUnit.value
speed.speedDisplayUnit.value,
carContext
)
)
setImage(
@@ -183,7 +185,8 @@ class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
setText(
formatCarUnitSpeed(
speed.displaySpeedMetersPerSecond.value,
speed.speedDisplayUnit.value
speed.speedDisplayUnit.value,
carContext
)
)
setImage(

View File

@@ -32,6 +32,7 @@ import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
@@ -81,7 +82,7 @@ class ChargepriceFragment : Fragment() {
}
.setPositiveButton(R.string.donate) { di, _ ->
di.dismiss()
findNavController().navigate(R.id.action_chargeprice_to_donateFragment)
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToDonateFragment())
}
.show()
}
@@ -167,7 +168,7 @@ class ChargepriceFragment : Fragment() {
chargepriceAdapter.myTariffsAll = it
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
it?.data?.let { chargepriceAdapter.submitList(it) }
chargepriceAdapter.submitList(it?.data ?: emptyList())
}
val connectorsAdapter = CheckableConnectorAdapter()
@@ -197,7 +198,7 @@ class ChargepriceFragment : Fragment() {
}
binding.btnSettings.setOnClickListener {
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToChargepriceSettingsFragment())
}
headerBinding.batteryRange.setLabelFormatter { value: Float ->

View File

@@ -28,7 +28,6 @@ import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.viewmodel.FavoritesViewModel
@@ -37,7 +36,6 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment() {
private lateinit var binding: FragmentFavoritesBinding
private lateinit var locationEngine: LocationEngine
private var toDelete: Favorite? = null
private var deleteSnackbar: Snackbar? = null
private lateinit var adapter: FavoritesAdapter
@@ -113,6 +111,32 @@ class FavoritesFragment : Fragment() {
binding.swipeRefresh.isRefreshing = false
}
}
vm.deletedFavorite.observe(viewLifecycleOwner) { fav ->
if (fav == null) {
deleteSnackbar?.dismiss()
return@observe
}
val snackbar = Snackbar.make(
requireView(),
getString(
R.string.deleted_item,
fav.charger.name
),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
vm.undoDeletion()
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
// if undo was not clicked, actually delete
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
vm.deletedFavorite.value = null
}
}
})
deleteSnackbar = snackbar
snackbar.show()
}
}
override fun onStart() {
@@ -127,41 +151,7 @@ class FavoritesFragment : Fragment() {
}
fun delete(fav: FavoriteWithDetail) {
val position =
vm.listData.value?.indexOfFirst { it.fav.favorite.favoriteId == fav.favorite.favoriteId }
?: return
// if there is already a profile to delete, delete it now
actuallyDelete()
deleteSnackbar?.dismiss()
toDelete = fav.favorite
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fav.charger.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null
adapter.notifyItemChanged(position)
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
// if undo was not clicked, actually delete
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
actuallyDelete()
}
}
})
deleteSnackbar = snackbar
snackbar.show()
} ?: run {
actuallyDelete()
}
}
private fun actuallyDelete() {
toDelete?.let { vm.deleteFavorite(it) }
toDelete = null
vm.deleteFavoriteWithUndo(fav)
}
private fun createTouchHelper(): ItemTouchHelper {

View File

@@ -230,7 +230,7 @@ class FilterProfilesFragment : Fragment() {
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fp.name),
getString(R.string.deleted_item, fp.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null

View File

@@ -3,10 +3,12 @@ package net.vonforst.evmap.fragment
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.location.Geocoder
import android.os.Build
import android.os.Bundle
import android.text.method.KeyListener
import android.view.*
@@ -73,17 +75,19 @@ 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
import net.vonforst.evmap.model.*
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.shouldUseImperialUnits
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.*
import net.vonforst.evmap.utils.boundingBox
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.utils.checkFineLocationPermission
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.formatDecimal
import net.vonforst.evmap.viewmodel.*
import java.io.IOException
import kotlin.collections.component1
@@ -194,6 +198,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
searchResultIcon = null
}
binding.detailAppBar.toolbar.popupTheme =
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _, insets ->
@@ -274,7 +281,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (prefs.appStartCounter > 5 && !prefs.opensourceDonationsDialogShown) {
try {
findNavController().navigate(R.id.action_map_to_opensource_donations)
findNavController().safeNavigate(MapFragmentDirections.actionMapToOpensourceDonations())
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
} catch (ignored: IllegalStateException) {
@@ -283,7 +290,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
/*if (!prefs.update060AndroidAutoDialogShown) {
try {
navController.navigate(R.id.action_map_to_update_060_androidauto)
navController.safeNavigate(MapFragmentDirections.actionMapToUpdate060AndroidAuto())
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
}
@@ -372,10 +379,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data ?: return@setOnClickListener
val extras =
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
findNavController().navigate(
R.id.action_map_to_chargepriceFragment,
ChargepriceFragmentArgs(charger).toBundle(),
null, extras
findNavController().safeNavigate(
MapFragmentDirections.actionMapToChargepriceFragment(charger),
extras
)
}
binding.detailView.btnChargerWebsite.setOnClickListener {
@@ -383,9 +389,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
charger.chargerUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
}
binding.detailView.btnLogin.setOnClickListener {
findNavController().navigate(
R.id.settings_data,
DataSettingsFragmentArgs(true).toBundle()
findNavController().safeNavigate(
MapFragmentDirections.actionMapToDataSettings(true)
)
}
binding.detailView.imgPredictionSource.setOnClickListener {
@@ -821,15 +826,69 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.drawable.ic_fault_report -> {
(activity as? MapsActivity)?.openUrl(charger.url)
}
R.drawable.ic_payment -> {
showPaymentMethodsDialog(charger)
}
R.drawable.ic_network -> {
charger.networkUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
}
}
}
}
onLongClickListener = {
val charger = vm.chargerDetails.value?.data
val clipboardManager =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
if (charger != null) {
when (it.icon) {
R.drawable.ic_address -> {
if (charger.address != null) {
val clip = ClipData.newPlainText(
getString(R.string.address),
charger.address.toString()
)
clipboardManager.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Snackbar.make(
requireView(),
R.string.copied,
Toast.LENGTH_SHORT
)
.show()
}
true
} else {
false
}
}
R.drawable.ic_location -> {
val clip = ClipData.newPlainText(
getString(R.string.coordinates),
charger.coordinates.formatDecimal()
)
clipboardManager.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Snackbar.make(
requireView(),
R.string.copied,
Toast.LENGTH_SHORT
)
.show()
}
true
}
else -> false
}
} else {
false
}
}
}
itemAnimator = null
layoutManager =
@@ -908,23 +967,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.scaleView.apply {
when (prefs.mapScale) {
"both" -> {
visibility = View.VISIBLE
if (prefs.showMapScale) {
visibility = View.VISIBLE
if (prefs.mapScaleMetersAndMiles) {
metersAndMiles()
} else {
if (shouldUseImperialUnits(requireContext())) {
milesOnly()
} else {
metersOnly()
}
}
"meters" -> {
visibility = View.VISIBLE
metersOnly()
}
"miles" -> {
visibility = View.VISIBLE
milesOnly()
}
"off" -> visibility = View.GONE
} else {
visibility = View.GONE
}
}
vm.mapPosition.observe(viewLifecycleOwner) {
@@ -1044,7 +1099,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.search.requestFocus()
binding.search.setSelection(locationName.length)
}
if (context.checkAnyLocationPermission()) {
if (context.checkAnyLocationPermission() && prefs.currentMapMyLocationEnabled) {
enableLocation(!positionSet, false)
positionSet = true
}
@@ -1226,26 +1281,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
MenuCompat.setGroupDividerEnabled(popup.menu, true)
popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
val navController = requireView().findNavController()
when (it.itemId) {
R.id.menu_edit_filters -> {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
lifecycleScope.launch {
vm.copyFiltersToCustom()
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
navController.safeNavigate(
MapFragmentDirections.actionMapToFilterFragment()
)
}
true
}
R.id.menu_manage_filter_profiles -> {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
requireView().findNavController().navigate(
R.id.action_map_to_filterProfilesFragment
navController.safeNavigate(
MapFragmentDirections.actionMapToFilterProfilesFragment()
)
true
}
else -> {
val profileId = profilesMap.inverse[it]
if (profileId != null) {
@@ -1326,11 +1384,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
filterView?.setOnLongClickListener {
// enable/disable filters
vm.toggleFilters()
// haptic feedback
filterView.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
)
// show snackbar
Snackbar.make(
requireView(), if (vm.filterStatus.value != FILTERS_DISABLED) {
@@ -1401,6 +1454,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
prefs.currentMapLocation = it.bounds.center
prefs.currentMapZoom = it.zoom
}
vm.myLocationEnabled.value?.let {
prefs.currentMapMyLocationEnabled = it
}
}
override fun onDestroy() {

View File

@@ -14,6 +14,7 @@ import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -21,6 +22,7 @@ import androidx.viewpager2.widget.ViewPager2
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.*
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
class OnboardingFragment : Fragment() {
@@ -82,7 +84,7 @@ class OnboardingFragment : Fragment() {
fun goToNext() {
if (binding.viewPager.currentItem == adapter.itemCount - 1) {
findNavController().navigate(R.id.action_onboarding_to_map)
findNavController().safeNavigate(OnboardingFragmentDirections.actionOnboardingToMap())
} else {
binding.viewPager.setCurrentItem(binding.viewPager.currentItem + 1, true)
}
@@ -225,7 +227,12 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.cbAcceptPrivacy.text =
Html.fromHtml(getString(R.string.accept_privacy, getString(R.string.privacy_link)))
HtmlCompat.fromHtml(
getString(
R.string.accept_privacy,
getString(R.string.privacy_link)
), HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.cbAcceptPrivacy.linksClickable = true
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethod.getInstance()
binding.btnGetStarted.visibility = View.INVISIBLE
@@ -243,9 +250,11 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
}
}
}
when (prefs.dataSource) {
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
if (prefs.dataSourceSet) {
when (prefs.dataSource) {
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
}
}
binding.btnGetStarted.setOnClickListener {

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.fragment.oauth
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
@@ -13,17 +14,25 @@ import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.transition.MaterialSharedAxis
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import java.lang.IllegalStateException
class OAuthLoginFragment : Fragment() {
companion object {
val ACTION_OAUTH_RESULT = "oauth_result"
val EXTRA_URL = "url"
}
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,10 +52,24 @@ class OAuthLoginFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
val navController = try {
findNavController()
} catch (e: IllegalStateException) {
null
// standalone in OAuthLoginActivity
}
if (navController != null) {
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
} else {
toolbar.title = getString(R.string.login)
toolbar.navigationIcon =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_back)
toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
}
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
val uri = Uri.parse(args.url)
@@ -68,7 +91,12 @@ class OAuthLoginFragment : Fragment() {
val result = Bundle()
result.putString("url", url.toString())
setFragmentResult(args.url, result)
findNavController().popBackStack()
context?.let {
LocalBroadcastManager.getInstance(it).sendBroadcast(
Intent(ACTION_OAUTH_RESULT).putExtra(EXTRA_URL, url)
)
}
navController?.popBackStack()
}
return url.host != uri.host

View File

@@ -16,6 +16,7 @@ import com.mikepenz.aboutlibraries.LibsBuilder
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -101,18 +102,17 @@ class AboutFragment : PreferenceFragmentCompat() {
.withAboutVersionShown(false)
.withAboutIconShown(false)
.withActivityTitle(getString(R.string.oss_licenses))
.withExcludedLibraries()
.start(requireActivity())
true
}
"donate" -> {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
findNavController().navigate(R.id.action_about_to_donateFragment)
findNavController().safeNavigate(AboutFragmentDirections.actionAboutToDonateFragment())
true
}
"github_sponsors" -> {
findNavController().navigate(R.id.action_about_to_github_sponsors)
findNavController().safeNavigate(AboutFragmentDirections.actionAboutToGithubSponsors())
true
}
"twitter" -> {

View File

@@ -28,7 +28,7 @@ class AndroidAutoSettingsFragment : BaseSettingsFragment() {
setPreferencesFromResource(R.xml.settings_android_auto, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"chargeprice_battery_range_android_auto_min", "chargeprice_battery_range_android_auto_max" -> {
updateRangePreferenceSummary()

View File

@@ -101,11 +101,12 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"chargeprice_my_vehicle" -> {
updateMyVehiclesSummary()
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}

View File

@@ -86,7 +86,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"search_provider" -> {
if (prefs.searchProvider == "google") {
@@ -141,18 +141,11 @@ class DataSettingsFragment : BaseSettingsFragment() {
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 uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)
val args = OAuthLoginFragmentArgs(
uri.toString(),
"https://auth.tesla.com/void/callback",
TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
).toBundle()
@@ -184,7 +177,8 @@ class DataSettingsFragment : BaseSettingsFragment() {
encryptedPrefs.teslaRefreshToken = response.refreshToken
} catch (e: IOException) {
view?.let {
Snackbar.make(it, R.string.connection_error, Snackbar.LENGTH_SHORT).show()
Snackbar.make(it, R.string.generic_connection_error, Snackbar.LENGTH_SHORT)
.show()
}
}
refreshTeslaAccountStatus()

View File

@@ -25,7 +25,7 @@ class DeveloperSettingsFragment : BaseSettingsFragment() {
setPreferencesFromResource(R.xml.settings_developer, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
}

View File

@@ -20,7 +20,7 @@ class SettingsFragment : BaseSettingsFragment() {
findPreference<Preference>("developer_options")?.isVisible = prefs.developerModeEnabled
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
}
}

View File

@@ -35,7 +35,7 @@ class UiSettingsFragment : BaseSettingsFragment() {
langPref.value = getAppLocale()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
"darkmode" -> {
updateNightMode(prefs)

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.DialogOpensourceDonationsBinding
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.MaterialDialogFragment
@@ -30,11 +31,11 @@ class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
}
binding.btnDonate.setOnClickListener {
prefs.opensourceDonationsDialogShown = true
findNavController().navigate(R.id.action_opensource_donations_to_donate)
findNavController().safeNavigate(OpensourceDonationsDialogFragmentDirections.actionOpensourceDonationsToDonate())
}
binding.btnGithubSponsors.setOnClickListener {
prefs.opensourceDonationsDialogShown = true
findNavController().navigate(R.id.action_opensource_donations_to_github_sponsors)
findNavController().safeNavigate(OpensourceDonationsDialogFragmentDirections.actionOpensourceDonationsToGithubSponsors())
}
}

View File

@@ -0,0 +1,17 @@
package net.vonforst.evmap.navigation
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.Navigator
fun NavController.safeNavigate(
direction: NavDirections,
navigatorExtras: Navigator.Extras? = null
) {
currentDestination?.getAction(direction.actionId) ?: return
if (navigatorExtras != null) {
navigate(direction, navigatorExtras)
} else {
navigate(direction)
}
}

View File

@@ -11,7 +11,6 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import co.anbora.labs.spatia.builder.SpatiaRoom
import co.anbora.labs.spatia.geometry.GeometryConverters
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
@@ -35,7 +34,7 @@ import net.vonforst.evmap.model.*
OCMCountry::class,
OCMOperator::class,
SavedRegion::class
], version = 21
], version = 22
)
@TypeConverters(Converters::class, GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -75,7 +74,8 @@ 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_19, MIGRATION_20, MIGRATION_21
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
MIGRATION_22
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -452,6 +452,13 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("DELETE FROM savedregion")
}
}
private val MIGRATION_22 = object : Migration(21, 22) {
override fun migrate(db: SupportSQLiteDatabase) {
// clear cache with this update
db.execSQL("DELETE FROM savedregion")
}
}
}
/**

View File

@@ -14,6 +14,24 @@ import java.time.Instant
class PreferenceDataSource(val context: Context) {
val sp = PreferenceManager.getDefaultSharedPreferences(context)
init {
if (sp.contains("map_scale")) {
// migration
val mapScale = sp.getString("map_scale", null)
sp.edit().putBoolean("map_scale_show", mapScale != "off")
.putBoolean("map_scale_meters_and_miles", mapScale == "both")
.putString(
"units", when (mapScale) {
"meters" -> "metric"
"miles" -> "imperial"
else -> "default"
}
)
.remove("map_scale")
.apply()
}
}
var dataSource: String
get() = sp.getString("data_source", "goingelectric")!!
set(value) {
@@ -243,8 +261,11 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("show_chargers_ahead_android_auto", value).apply()
}
val predictionEnabled: Boolean
var predictionEnabled: Boolean
get() = sp.getBoolean("prediction_enabled", true)
set(value) {
sp.edit().putBoolean("prediction_enabled", value).apply()
}
var developerModeEnabled: Boolean
get() = sp.getBoolean("dev_mode_enabled", false)
@@ -252,8 +273,14 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("dev_mode_enabled", value).apply()
}
val mapScale: String
get() = sp.getString("map_scale", null) ?: "both"
val showMapScale: Boolean
get() = sp.getBoolean("map_scale_show", true)
val mapScaleMetersAndMiles: Boolean
get() = sp.getBoolean("map_scale_meters_and_miles", true)
val units: String
get() = sp.getString("units", null) ?: "default"
var currentMapLocation: LatLng
get() = sp.getLatLng("current_map_location") ?: LatLng(50.113388, 9.252536)
@@ -267,6 +294,12 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putFloat("current_map_zoom", value).apply()
}
var currentMapMyLocationEnabled: Boolean
get() = sp.getBoolean("current_map_my_location_enabled", false)
set(value) {
sp.edit().putBoolean("current_map_my_location_enabled", value).apply()
}
var privacyAccepted: Boolean
get() = sp.getBoolean("privacy_accepted", false)
set(value) {

View File

@@ -9,6 +9,7 @@ import android.text.TextPaint
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
@@ -27,7 +28,6 @@ import kotlin.math.roundToInt
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
var barMargin = 2 * dp
@@ -35,7 +35,9 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
var legendLineLength = 4 * dp
var legendLineWidth = 1 * dp
var dashLength = 4 * dp
var bubbleTextSize = (12 * sp).roundToInt()
var bubbleTextSize =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, context.resources.displayMetrics)
.roundToInt()
var bubblePadding = (6 * dp).roundToInt()
var selectedBar: Int = 0
var bubbleStrokeWidth = 1 * dp

View File

@@ -309,9 +309,9 @@ fun time(value: Int): String {
else "%d:%02d h".format(h, min)
}
fun distance(meters: Number?): String? {
fun distance(meters: Number?, ctx: Context): String? {
if (meters == null) return null
if (shouldUseImperialUnits()) {
if (shouldUseImperialUnits(ctx)) {
val ft = meters.toDouble() / meterPerFt
val mi = meters.toDouble() / 1e3 / kmPerMile
return when {

View File

@@ -116,6 +116,7 @@ class ChargepriceViewModel(
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
value = state["chargePrices"] ?: Resource.loading(null)
listOf(
vehicle,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors,

View File

@@ -33,7 +33,7 @@ class FavoritesViewModel(application: Application) :
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
addSource(favorites) { favorites ->
if (favorites != null) {
reloadAvailability()
reloadAvailability(forceReload = false)
} else {
value = null
}
@@ -41,9 +41,10 @@ class FavoritesViewModel(application: Application) :
}
}
fun reloadAvailability(callback: (() -> Unit)? = null) {
fun reloadAvailability(forceReload: Boolean = true, callback: (() -> Unit)? = null) {
val favorites = favorites.value ?: return
val chargers = favorites.map { it.charger }
val previous = availability.value
viewModelScope.launch {
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
@@ -54,8 +55,13 @@ class FavoritesViewModel(application: Application) :
chargers.map { charger ->
async {
data[charger.id] = availabilityRepo.getAvailability(charger)
availability.value = data
if (!forceReload && previous?.get(charger.id)?.status == Status.SUCCESS) {
data[charger.id] = previous[charger.id]!!
availability.value = data
} else {
data[charger.id] = availabilityRepo.getAvailability(charger)
availability.value = data
}
}
}.awaitAll()
callback?.invoke()
@@ -117,6 +123,24 @@ class FavoritesViewModel(application: Application) :
}
}
val deletedFavorite: MutableLiveData<FavoriteWithDetail?> by lazy {
MutableLiveData<FavoriteWithDetail?>()
}
fun deleteFavoriteWithUndo(fav: FavoriteWithDetail) {
deletedFavorite.value = fav
deleteFavorite(fav.favorite)
}
fun undoDeletion() {
deletedFavorite.value?.let {
viewModelScope.launch {
insertFavorite(it.charger)
}
deletedFavorite.value = null
}
}
fun deleteFavorite(fav: Favorite) {
viewModelScope.launch {
db.favoritesDao().delete(fav)

View File

@@ -24,6 +24,8 @@ import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.FronyxEvseIdResponse
import net.vonforst.evmap.api.fronyx.FronyxStatus
import net.vonforst.evmap.api.fronyx.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.openchargemap.OCMConnection
@@ -250,155 +252,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
it.data?.extraData as? TeslaGraphQlApi.Pricing
}
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
private val predictionRepository = PredictionRepository(application)
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
availability.switchMap { av ->
if (!prefs.predictionEnabled) return@switchMap null
av.data?.evseIds?.let { evseIds ->
liveData {
emit(Resource.loading(null))
val charger = charger.value?.data ?: return@liveData
val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(
it.type
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
if (allEvseIds.isEmpty()) {
emit(Resource.success(emptyList()))
return@liveData
}
try {
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
if (result.size == allEvseIds.size) {
emit(Resource.success(result))
} else {
emit(Resource.error("not all EVSEIDs found", null))
}
} catch (e: IOException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: HttpException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: JsonDataException) {
// malformed JSON response from fronyx API
emit(Resource.error(e.message, null))
e.printStackTrace()
}
}
} ?: liveData { emit(Resource.success(null)) }
}
}
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 }
}
}
}
}
}
}
private val predictedChargepoints = charger.map {
it.data?.let { charger ->
charger.chargepoints.filter {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(it.type).any {
filtered.contains(
it
)
}
} ?: true
}
}
}
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
}
}
}
val predictionDescription: LiveData<String?> by lazy {
predictedChargepoints.map { predictedChargepoints ->
if (predictedChargepoints == null) return@map null
val allChargepoints = charger.value?.data?.chargepoints ?: return@map null
val predictedChargepointTypes = predictedChargepoints.map { it.type }.distinct()
if (allChargepoints == predictedChargepoints) {
null
} else if (predictedChargepointTypes.size == 1) {
application.getString(
R.string.prediction_only,
nameForPlugType(application.stringProvider(), predictedChargepointTypes[0])
)
} else {
application.getString(
R.string.prediction_only,
application.getString(R.string.prediction_dc_plugs_only)
)
}
val predictionData: LiveData<PredictionData> = availability.switchMap { av ->
liveData {
val charger = charger.value?.data ?: return@liveData
emit(predictionRepository.getPredictionData(charger, av.data, filteredConnectors.value))
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true" />

View File

@@ -37,6 +37,8 @@
<import type="java.time.Duration" />
<import type="net.vonforst.evmap.api.fronyx.PredictionData" />
<variable
name="charger"
type="Resource&lt;ChargeLocation&gt;" />
@@ -50,20 +52,8 @@
type="Resource&lt;ChargeLocationStatus&gt;" />
<variable
name="predictionGraph"
type="Map&lt;ZonedDateTime, Double&gt;" />
<variable
name="predictionMaxValue"
type="Double" />
<variable
name="predictionIsPercentage"
type="Boolean" />
<variable
name="predictionDescription"
type="String" />
name="predictionData"
type="PredictionData" />
<variable
name="filteredAvailability"
@@ -147,7 +137,7 @@
android:gravity="end"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{BindingAdaptersKt.distance(distance)}"
android:text="@{BindingAdaptersKt.distance(distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
@@ -367,11 +357,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{predictionIsPercentage ? @string/average_utilization : @string/utilization_prediction}"
android:text="@{predictionData.isPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionGraph != null}"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
@@ -381,9 +371,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@{predictionDescription}"
android:text="@{predictionData.description}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
@@ -395,7 +385,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
@@ -407,13 +397,13 @@
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
app:data="@{predictionGraph}"
app:goneUnless="@{predictionGraph != null}"
app:data="@{predictionData.predictionGraph}"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView8"
app:maxValue="@{predictionMaxValue}"
app:isPercentage="@{predictionIsPercentage}"
app:maxValue="@{predictionData.maxValue}"
app:isPercentage="@{predictionData.isPercentage}"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
@@ -427,7 +417,7 @@
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
@@ -439,7 +429,7 @@
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{predictionGraph != null}"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<ImageView

View File

@@ -27,7 +27,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialogTitle"
app:layout_constraintBottom_toTopOf="@id/btnCancel">
app:layout_constraintBottom_toTopOf="@id/btnCancel"
android:requiresFadingEdge="vertical">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView20"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/referrals"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary" />
<TextView
android:id="@+id/textView21"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/referrals_info" />
<Button
android:id="@+id/referral_tesla"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_tesla"
app:icon="@drawable/ic_tesla" />
</LinearLayout>

View File

@@ -197,10 +197,7 @@
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionGraph="@{vm.predictionGraph}"
app:predictionMaxValue="@{vm.predictionMaxValue}"
app:predictionIsPercentage="@{vm.predictionIsPercentage}"
app:predictionDescription="@{vm.predictionDescription}"
app:predictionData="@{vm.predictionData}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"

View File

@@ -4,7 +4,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:fillViewport="true">
android:fillViewport="true"
android:requiresFadingEdge="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -17,7 +18,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="32dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/dataSourceHint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
@@ -28,6 +29,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:gravity="center"
@@ -36,7 +38,9 @@
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/welcomeText2"
@@ -72,12 +76,12 @@
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:layout_marginBottom="24dp"
android:breakStrategy="balanced"
android:gravity="center"
android:text="@string/data_sources_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?android:textColorSecondary"
android:breakStrategy="balanced"
app:layout_constraintBottom_toTopOf="@+id/cb_accept_privacy"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
@@ -89,14 +93,14 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="32dp"
android:layout_marginBottom="24dp"
android:paddingStart="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
tools:text="@string/accept_privacy"
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/accept_privacy" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -62,7 +62,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@{BindingAdaptersKt.distance(item.distanceMeters)}"
android:text="@{BindingAdaptersKt.distance(item.distanceMeters, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.distanceMeters != null}"
app:layout_constraintEnd_toEndOf="@+id/icon"

View File

@@ -97,7 +97,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@{BindingAdaptersKt.distance(item.distance)}"
android:text="@{BindingAdaptersKt.distance(item.distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.distance != null}"
app:layout_constraintEnd_toStartOf="@id/btnDelete"

View File

@@ -32,6 +32,9 @@
<action
android:id="@+id/action_map_to_opensource_donations"
app:destination="@id/opensource_donations" />
<action
android:id="@+id/action_map_to_data_settings"
app:destination="@id/settings_data" />
<argument
android:name="locationName"
android:defaultValue="@null"

View File

@@ -113,7 +113,7 @@
<string name="goingelectric_forum">Forenthread bei GoingElectric.de</string>
<string name="contact">Kontakt</string>
<string name="menu_report_new_charger">Ladesäule melden</string>
<string name="edit_at_datasource">bei %s bearbeiten</string>
<string name="edit_at_datasource">Bei %s bearbeiten</string>
<string name="categories">Kategorien</string>
<string name="category_car_dealership">Autohaus</string>
<string name="category_service_on_motorway">Autobahnraststätte</string>
@@ -161,7 +161,7 @@
<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>
<string name="deleted_item">„%s” gelöscht</string>
<string name="undo">Rückgängig</string>
<string name="rename">Umbenennen</string>
<string name="charging_barrierfree">Ohne Vertrag / Registrierung nutzbar</string>
@@ -275,6 +275,7 @@
<string name="about_contributors">Mitwirkende</string>
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
<string name="utilization_prediction">Auslastungsprognose</string>
<string name="powered_by_fronyx">powered by fronyx</string>
<string name="prediction_help">Die Prognose basiert auf Faktoren wie Wochentag, Uhrzeit und Nutzung in der Vergangenheit. So kannst du stark ausgelastete Ladesäulen vermeiden. Keine Garantie.</string>
<string name="prediction_time_colon">%s Uhr:</string>
<plurals name="prediction_number_available">
@@ -304,8 +305,8 @@
<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="login">Anmelden</string>
<string name="login_error">Anmeldung fehlgeschlagen</string>
<string name="tesla_pricing_owners">Nur Tesla-Fahrzeuge:</string>
<string name="tesla_pricing_members">Tesla-Fahrzeuge &amp; Mitglieder:</string>
<string name="tesla_pricing_others">Andere Kunden:</string>
@@ -314,17 +315,18 @@
<string name="tesla_pricing_blocking_fee">Blockiergebühr: %s</string>
<string name="average_utilization">Durchschnittliche Auslastung</string>
<string name="website">Website</string>
<string name="pref_map_scale">Kartenmaßstab</string>
<string name="pref_map_scale_both">Meter und Meilen</string>
<string name="pref_map_scale_meters">Meter</string>
<string name="pref_map_scale_miles">Meilen</string>
<string name="pref_map_scale_off">aus</string>
<string name="pref_map_scale">Kartenmaßstab zeigen</string>
<string name="pref_map_scale_meters_and_miles">Meilen und Meter am Kartenmaßstab</string>
<string name="pref_units">Einheiten</string>
<string name="pref_units_default">Geräteeinstellung verwenden</string>
<string name="pref_units_metric">Metrisch</string>
<string name="pref_units_imperial">Imperial</string>
<string name="data_retrieved_at">Daten abgerufen %s</string>
<string name="settings_caching">Cache</string>
<string name="settings_cache_count">Cache-Größe</string>
<string name="settings_cache_clear">Cache leeren</string>
<string name="settings_cache_clear_summary">Löscht alle gespeicherten Ladestationen außer Favoriten</string>
<string name="settings_cache_count_summary">%d Ladestationen gespeichert, %.1f MB</string>
<string name="settings_cache_count_summary">%1$d Ladestationen gespeichert, %2$.1f MB</string>
<string name="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>
<string name="auto_no_favorites_found">Keine Favoriten gefunden</string>
@@ -347,8 +349,8 @@
<string name="auto_heading">Fahrtrichtung</string>
<string name="auto_settings">Einstellungen</string>
<string name="welcome_android_auto">Android Auto-Unterstützung</string>
<string name="welcome_android_auto_detail">Auf unterstützen Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.</string>
<string name="sounds_cool">klingt cool</string>
<string name="welcome_android_auto_detail">Auf unterstützten Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.</string>
<string name="sounds_cool">Klingt cool</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap konnte das Fahrzeugmodell nicht erkennen.</string>
<string name="auto_chargeprice_vehicle_unknown">Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%1$s %2$s).</string>
@@ -358,7 +360,12 @@
<string name="selecting_none">alle Einträge abgewählt</string>
<string name="loading">Lade…</string>
<string name="auto_multipage_goto">Seite %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_multipage">(%1$d/%2$d)</string>
<string name="reload">Neu laden</string>
<string name="accept_privacy"><![CDATA[Ich habe die <a href=\"%s\">Datenschutzerklärung</a> von EVMap gelesen und bin damit einverstanden.]]></string>
<string name="referrals">Empfehlungslinks</string>
<string name="referrals_info">Du kannst auch einen der Empfehlungslinks unten benutzen, um den Entwickler mit deinem Kauf zu unterstützen.</string>
<string name="referral_tesla">Tesla</string>
<string name="generic_connection_error">Daten konnten nicht geladen werden</string>
<string name="copied">In Zwischenablage kopiert</string>
</resources>

View File

@@ -55,7 +55,7 @@
<string name="filter_exclude_faults">Exclure les chargeurs avec des défauts signalés</string>
<string name="charge_cards">Méthodes de paiement</string>
<string name="goingelectric_forum">Fil de discussion du forum sur GoingElectric.de</string>
<string name="edit_at_datasource">modifier à %s</string>
<string name="edit_at_datasource">Modifier à %s</string>
<string name="categories">Catégories</string>
<string name="category_car_dealership">Concessionnaire automobile</string>
<string name="category_public_authorities">Pouvoirs publics</string>
@@ -75,7 +75,7 @@
<string name="welcome_2_detail">Cela peut également être vu dans \"À propos\" → \"Foire aux questions\"</string>
<string name="donation_dialog_title">Merci d\'utiliser EVMap</string>
<string name="chargeprice_donation_dialog_title">Vous êtes un vrai chasseur de bonnes affaires !</string>
<string name="deleted_filterprofile">\"%s\" supprimé</string>
<string name="deleted_item">\"%s\" supprimé</string>
<string name="undo">Annuler</string>
<string name="rename">Renommer</string>
<string name="verified">vérifié</string>
@@ -308,7 +308,7 @@
<string name="auto_range">Autonomie</string>
<string name="auto_speed">Vitesse</string>
<string name="welcome_android_auto">Prise en charge dAndroid Auto</string>
<string name="sounds_cool">ça a l\'air cool</string>
<string name="sounds_cool">Ça a l\'air cool</string>
<string name="auto_chargeprice_vehicle_unknown">Aucun des véhicules sélectionnés dans l\'application ne correspond à ce véhicule (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Plusieurs véhicules sélectionnés dans l\'application correspondent à ce véhicule (%1$s %2$s).</string>
<string name="selecting_all">tous les éléments sélectionnés</string>
@@ -330,5 +330,5 @@
<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>
<string name="auto_multipage">(%1$d/%2$d)</string>
</resources>

View File

@@ -127,7 +127,7 @@
<string name="category_amusement_park">Fornøyelsespark</string>
<string name="category_cinema">Kino</string>
<string name="category_parking_multi">Parkeringshus</string>
<string name="edit_at_datasource">rediger på %s</string>
<string name="edit_at_datasource">Rediger på %s</string>
<string name="category_camping">Campingplass</string>
<string name="category_service_on_motorway">Rasteplass (på motorvei)</string>
<string name="category_shopping_mall">Kjøpesenter</string>
@@ -144,7 +144,7 @@
<string name="save_profile_enter_name">Skriv inn navnet på filterprofilen:</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler</string>
<string name="chargeprice_donation_dialog_title">Du er en sann gjerrigknark.</string>
<string name="deleted_filterprofile">Slettet «%s»</string>
<string name="deleted_item">Slettet «%s»</string>
<string name="charging_barrierfree">Kan brukes uten registrering</string>
<string name="welcome_1">Finn kjøretøyladere der du er</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt</string>
@@ -326,9 +326,23 @@
<string name="welcome_android_auto_detail">Du kan også bruke EVMap inne i Android Auto på bilder som støtter dette ved å velge det i Android Auto-menyen.</string>
<string name="settings_android_auto_chargeprice_range">Prissammenligning for laderekkevidde fordelt på pris</string>
<string name="selecting_all">valgte alle elementene</string>
<string name="sounds_cool">den er grei</string>
<string name="sounds_cool">Den er grei</string>
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
<string name="loading">Laster inn …</string>
<string name="auto_multipage_goto">Side %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_multipage">(%1$d/%2$d)</string>
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
<string name="website">Nettside</string>
<string name="pref_tesla_account_enabled">Innlogget som %s</string>
<string name="pref_units">Enheter</string>
<string name="log_out">Logg ut</string>
<string name="logging_in">Logger inn …</string>
<string name="realtime_data_login_needed">Tesla-konto kreves for sanntidsdata</string>
<string name="generic_connection_error">Kunne ikke laste inn data</string>
<string name="logged_out">Utlogget</string>
<string name="pref_tesla_account">Tesla-konto</string>
<string name="tesla_pricing_others">Andre kunder:</string>
<string name="referral_tesla">Tesla</string>
<string name="pref_units_default">Enhetsforvalg</string>
<string name="login">Logg inn</string>
</resources>

View File

@@ -145,7 +145,7 @@
<string name="donation_dialog_title">Bedankt om EVMap te gebruiken</string>
<string name="chargeprice_donation_dialog_title">Jij bent een echte koopjeszoeker!</string>
<string name="chargeprice_donation_dialog_detail">Blijkbaar maak je dankbaar gebruik van de prijsvergelijkingen. Met een donatie kan je de kosten voor deze data helpen dragen.</string>
<string name="deleted_filterprofile">“%s” verwijderd</string>
<string name="deleted_item">“%s” verwijderd</string>
<string name="undo">Ongedaan maken</string>
<string name="rename">Hernoem</string>
<plurals name="charge_cards_compatible_num">
@@ -287,7 +287,7 @@
<string name="show_less">minder…</string>
<string name="map_type">Kaarttype</string>
<string name="map_details">Kaartdetails</string>
<string name="edit_at_datasource">aanpassen op %s</string>
<string name="edit_at_datasource">Aanpassen op %s</string>
<string name="charge_cards">Betaalmethoden</string>
<string name="pref_map_provider">Kaartaanbieder</string>
<string name="twitter">Twitter</string>
@@ -319,7 +319,7 @@
<string name="auto_settings">Instellingen</string>
<string name="welcome_android_auto">Android Auto support</string>
<string name="welcome_android_auto_detail">Je kan EVMap ook gebruiken in Android Auto op ondersteunde voertuigen. Selecteer gewoon de EVMap app in het Android Auto menu.</string>
<string name="sounds_cool">klinkt cool</string>
<string name="sounds_cool">Klinkt cool</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap kon je voertuigtype niet bepalen.</string>
<string name="auto_chargers_ahead">Alleen laadpunten in rijrichting</string>
<string name="settings_android_auto_chargeprice_range">Laadbereik voor prijsvergelijking</string>
@@ -327,6 +327,6 @@
<string name="selecting_none">alle items gedeselecteerd</string>
<string name="loading">Laden…</string>
<string name="auto_multipage_goto">Pagina %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_multipage">(%1$d/%2$d)</string>
<string name="auto_chargeprice_vehicle_unknown">Geen enkel voertuig geselecteerd in de app komt overeen met dit voertuig (%1$s %2$s).</string>
</resources>

View File

@@ -8,12 +8,12 @@
<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="deleted_item">“%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="chargeprice_select_connector">Escolha 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>
@@ -58,7 +58,7 @@
<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">Todas selecionadas</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>
@@ -76,7 +76,7 @@
<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="edit_at_datasource">Editar 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>
@@ -96,7 +96,7 @@
<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_2">Cada cor corresponde à potência máxima do carregador</string>
<string name="welcome_to_evmap">Bem-vindo(a) ao EVMap</string>
<string name="pref_darkmode_always_off">Sempre desligado</string>
<string name="welcome_2_title">Escolha a potência</string>
@@ -153,14 +153,14 @@
<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="pref_search_provider">Provedor 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="pref_search_provider_info">As pesquisas são caras, especialmente se o Google Maps for utilizado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
<string name="github_sponsors_desc">Apoie o EVMap através do GitHub</string>
<string name="unnamed_filter_profile">Filtro sem nome</string>
<string name="deleted_recent_search_results">As pesquisas recentes foram eliminadas</string>
@@ -319,17 +319,13 @@
<string name="average_utilization">Utilização média</string>
<string name="tesla_pricing_owners">Apenas veículos Tesla:</string>
<string name="website">Website</string>
<string name="pref_map_scale_off">desativar</string>
<string name="pref_map_scale_both">metros e milhas</string>
<string name="pref_map_scale_meters">metros</string>
<string name="pref_map_scale_miles">milhas</string>
<string name="pref_map_scale">Barra de escala do mapa</string>
<string name="pref_map_scale">Mostrar barra de escala do mapa</string>
<string name="data_retrieved_at">Informação atualizada %s</string>
<string name="settings_cache_count">Tamanho da cache</string>
<string name="settings_cache_clear">Limpar cache</string>
<string name="settings_cache_count_summary">%d carregadores na base de dados, %.1f MB</string>
<string name="settings_cache_count_summary">%1$d carregadores na base de dados, %2$.1f MB</string>
<string name="settings_caching">Caching (base de dados local)</string>
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados na base de dados local, com a exceção dos seus favoritos</string>
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados localmente, com a exceção dos seus favoritos</string>
<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>
@@ -345,7 +341,7 @@
<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="auto_multipage">(%1$d/%2$d)</string>
<string name="settings_android_auto_chargeprice_range">Escala de carregamento para comparação de preços</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>
@@ -364,4 +360,16 @@
<string name="auto_chargeprice_vehicle_unknown">Nenhum dos veículos selecionados na app corresponde a este veículo (%1$s %2$s).</string>
<string name="auto_chargers_ahead">Apenas carregadores na direção do destino</string>
<string name="sounds_cool">Continuar</string>
<string name="reload">Recarregar</string>
<string name="accept_privacy"><![CDATA[Li e aceito a <a href=\"%s\">política de privacidade</a> do EVMap.]]></string>
<string name="referrals">Links de afiliado</string>
<string name="referral_tesla">Tesla</string>
<string name="pref_units">Unidades</string>
<string name="pref_map_scale_meters_and_miles">Milhas e metros na barra de escala do mapa</string>
<string name="pref_units_default">Padrão do dispositivo</string>
<string name="pref_units_metric">Métrico</string>
<string name="pref_units_imperial">Imperial</string>
<string name="referrals_info">Também pode usar um dos seguintes links de afiliado para apoiar o desenvolvedor da app com a sua compra.</string>
<string name="generic_connection_error">Não foi possível carregar a informação</string>
<string name="powered_by_fronyx">previsão feita por fronyx</string>
</resources>

View File

@@ -112,7 +112,7 @@
<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="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>
@@ -160,7 +160,7 @@
<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="deleted_item">“%s” a fost sters</string>
<string name="undo">Anuleaza</string>
<string name="rename">Redenumeste</string>
<string name="charging_barrierfree">Utilizabile fara inregistrare</string>

View File

@@ -66,16 +66,14 @@
<item>goingelectric</item>
<item>openchargemap</item>
</string-array>
<string-array name="pref_map_scale_names">
<item>@string/pref_map_scale_both</item>
<item>@string/pref_map_scale_meters</item>
<item>@string/pref_map_scale_miles</item>
<item>@string/pref_map_scale_off</item>
<string-array name="pref_units_names">
<item>@string/pref_units_default</item>
<item>@string/pref_units_metric</item>
<item>@string/pref_units_imperial</item>
</string-array>
<string-array name="pref_map_scale_values" translatable="false">
<item>both</item>
<item>meters</item>
<item>miles</item>
<item>off</item>
<string-array name="pref_units_values" translatable="false">
<item>default</item>
<item>metric</item>
<item>imperial</item>
</string-array>
</resources>

View File

@@ -28,9 +28,13 @@
pt2121\n
nautilusx\n
Bobby Galati\n
programmin1
programmin1\n
Jean-BaptisteC
</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="donate_link" translatable="false">https://ev-map.app/donate/</string>
<string name="tesla_referral_link" translatable="false">http://ts.la/johan94494</string>
<string name="copyright_summary">©20202023 Johan von Forstner and contributors</string>
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
</resources>

View File

@@ -113,7 +113,7 @@
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
<string name="contact">Contact</string>
<string name="menu_report_new_charger">New charger</string>
<string name="edit_at_datasource">edit at %s</string>
<string name="edit_at_datasource">Edit at %s</string>
<string name="categories">Categories</string>
<string name="category_car_dealership">Car Dealership</string>
<string name="category_service_on_motorway">Service area (on motorway)</string>
@@ -161,7 +161,7 @@
<string name="donation_dialog_detail">EVMap is open source and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
<string name="chargeprice_donation_dialog_detail">You make great use of the price comparison feature. Please help cover the costs for this data by supporting EVMap with a donation.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
<string name="deleted_item">Deleted “%s”</string>
<string name="undo">Undo</string>
<string name="rename">Rename</string>
<string name="charging_barrierfree">Usable without registration</string>
@@ -275,6 +275,7 @@
<string name="about_contributors">Contributors</string>
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
<string name="utilization_prediction">Utilization prediction</string>
<string name="powered_by_fronyx">powered by fronyx</string>
<string name="prediction_help">The prediction is based on factors like day of the week, time of day and past usage, so that you can avoid overcrowded chargers. No guarantee.</string>
<string name="prediction_time_colon">%s:</string>
<plurals name="prediction_number_available">
@@ -304,7 +305,7 @@
<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">Log in</string>
<string name="login_error">Login failed</string>
<string name="tesla_pricing_owners">Tesla vehicles only:</string>
<string name="tesla_pricing_members">Tesla vehicles &amp; members:</string>
@@ -314,17 +315,18 @@
<string name="tesla_pricing_blocking_fee">Blocking fee: %s</string>
<string name="average_utilization">Average Utilization</string>
<string name="website">Website</string>
<string name="pref_map_scale">Map scale bar</string>
<string name="pref_map_scale_both">meters and miles</string>
<string name="pref_map_scale_meters">meters</string>
<string name="pref_map_scale_miles">miles</string>
<string name="pref_map_scale_off">off</string>
<string name="pref_map_scale">Show map scale bar</string>
<string name="pref_map_scale_meters_and_miles">Miles and meters on map scale bar</string>
<string name="pref_units">Units</string>
<string name="pref_units_default">Device default</string>
<string name="pref_units_metric">Metric</string>
<string name="pref_units_imperial">Imperial</string>
<string name="data_retrieved_at">Data retrieved %s</string>
<string name="settings_caching">Caching</string>
<string name="settings_cache_count">Cache size</string>
<string name="settings_cache_clear">Clear cache</string>
<string name="settings_cache_clear_summary">Deletes all cached chargers except favorites</string>
<string name="settings_cache_count_summary">%d chargers cached, %.1f MB</string>
<string name="settings_cache_count_summary">%1$d chargers cached, %2$.1f MB</string>
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
<string name="auto_no_chargers_found">No nearby chargers found</string>
<string name="auto_no_favorites_found">No favorites found</string>
@@ -348,7 +350,7 @@
<string name="auto_settings">Settings</string>
<string name="welcome_android_auto">Android Auto support</string>
<string name="welcome_android_auto_detail">You can also use EVMap from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
<string name="sounds_cool">sounds cool</string>
<string name="sounds_cool">Sounds cool</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap could not determine your vehicle model.</string>
<string name="auto_chargeprice_vehicle_unknown">None of the vehicles selected in the app matches this vehicle (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Multiple vehicles selected in the app match this vehicle (%1$s %2$s).</string>
@@ -358,7 +360,12 @@
<string name="selecting_none">deselected all items</string>
<string name="loading">Loading…</string>
<string name="auto_multipage_goto">Page %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_multipage">(%1$d/%2$d)</string>
<string name="reload">Reload</string>
<string name="accept_privacy"><![CDATA[I have read and accepted EVMap\'s <a href=\"%s\">privacy policy</a>.]]></string>
<string name="referrals">Referral links</string>
<string name="referrals_info">You can also use one of the referral links below to support the developer with your purchase.</string>
<string name="referral_tesla">Tesla</string>
<string name="generic_connection_error">Could not load data</string>
<string name="copied">Copied to clipboard</string>
</resources>

View File

@@ -73,6 +73,13 @@
<!-- this is necessary to make sure the dialog gets "pushed up" when the keyboard appears -->
<item name="android:windowTranslucentStatus">false</item>
<item name="dialogCornerRadius">28dp</item>
<item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
</style>
<style name="MaterialAlertDialog.App" parent="MaterialAlertDialog.Material3">
<!-- reduce insets from 80dp to 24dp -->
<item name="backgroundInsetTop">24dp</item>
<item name="backgroundInsetBottom">24dp</item>
</style>
<style name="CarAppTheme">

View File

@@ -8,6 +8,13 @@
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="%s" />
<ListPreference
android:key="units"
android:title="@string/pref_units"
android:entries="@array/pref_units_names"
android:entryValues="@array/pref_units_values"
android:defaultValue="default"
android:summary="%s" />
<ListPreference
android:key="darkmode"
@@ -22,13 +29,15 @@
android:summaryOn="@string/pref_map_rotate_gestures_on"
android:summaryOff="@string/pref_map_rotate_gestures_off"
android:defaultValue="true" />
<ListPreference
android:key="map_scale"
<CheckBoxPreference
android:key="map_scale_show"
android:title="@string/pref_map_scale"
android:entries="@array/pref_map_scale_names"
android:entryValues="@array/pref_map_scale_values"
android:defaultValue="both"
android:summary="%s" />
android:defaultValue="true" />
<CheckBoxPreference
android:key="map_scale_meters_and_miles"
android:title="@string/pref_map_scale_meters_and_miles"
android:defaultValue="true" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"

View File

@@ -10,14 +10,17 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ApplicationProvider
import net.vonforst.evmap.FakeAndroidKeyStore
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@Config(sdk = [33]) // Robolectric does not yet support SDK 34
class CarAppTest {
private val testCarContext =
TestCarContext.createCarContext(ApplicationProvider.getApplicationContext()).apply {
@@ -43,7 +46,7 @@ class CarAppTest {
val screenCreated =
testCarContext.getCarService(TestScreenManager::class.java).screensPushed.last()
// location permission required
assert(screenCreated is PermissionScreen)
// accept privacy required
assert(screenCreated is AcceptPrivacyScreen)
}
}

View File

@@ -1,35 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.8.21'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.3'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
classpath "pt.jcosta.resourceplaceholders:plugin:0.7"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
//noinspection JcenterRepositoryObsolete
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

35
build.gradle.kts Normal file
View File

@@ -0,0 +1,35 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion by extra("1.9.10")
val aboutLibsVersion by extra("10.9.1")
val navVersion by extra("2.7.5")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibsVersion")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
classpath("pt.jcosta.resourceplaceholders:plugin:0.7")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
//noinspection JcenterRepositoryObsolete
maven { setUrl("https://jitpack.io") }
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -23,9 +23,7 @@ Install Android Auto
If you haven't already, install the
[Android Auto](https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead)
and
[Android Auto for phone screens](https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead.phonescreen)
apps on your test device from the Google Play Store.
app on your test device from the Google Play Store.
If you are using the Android Emulator, the Play Store may show the Android Auto app as incompatible.
In that case, download the APK for the newest version from a site like
@@ -33,12 +31,12 @@ In that case, download the APK for the newest version from a site like
(choosing the correct architecture for your emulator - x86_64, x86 or ARM)
and drag it onto the running emulator window to install.
Starting the DHU
----------------
Starting and connecting to the DHU
----------------------------------
(see also the corresponding section on
the [Android Developers site](https://developer.android.com/training/cars/testing#running-dhu))
1. Start the Android Auto for phone screens app, tap the menu icon on the top left to go to settings
1. Go to Android Auto settings (Settings app -> Connected devices -> Connection preferences -> Android Auto)
2. Scroll all the way down to the app version, tap it 10 times
3. Click *OK* in the dialog that appears to enable developer mode
4. In the menu on the top left, tap *Start head unit server*

View File

@@ -29,6 +29,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
<string name="fronyx_key" translatable="false">
insert your Fronyx key here
</string>
<string name="acra_credentials" translatable="false">
insert your ACRA crash reporting credentials here
</string>
</resources>
```
@@ -186,4 +189,12 @@ The API is not publically available, contact [fronyx](https://fronyx.io/contact-
key and documentation.
If you don't want to test this functionality, simply leave the API key blank.
</details>
</details>
Crash reporting
---------------
Crash reporting for release builds is done using [ACRA](https://github.com/ACRA/acra).
This should not be needed for debugging.
If you still want to try it out, you can host any compatible backend such as
[Acrarium](https://github.com/F43nd1r/Acrarium/) yourself.

View File

@@ -0,0 +1,5 @@
Fehler behoben:
- GoingElectric: Filteroption "CCS" erschien doppelt
- Korrekturen für kleine Bildschirme
- Farbe eines Menüs im dunklen Design korrigiert
- Abstürze behoben

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Neue Einstellung für Maßeinheiten
- Anpassungen für Android 14
- Android Auto: Weitere Detailbeschreibungen zu den Ladestationen
- Android Auto: Löschbutton in der Filterliste
Fehler behoben:
- Fehler beim Laden der EnBW Echtzeitdaten

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Beim Start der App wird nun der zuletzt gesehene Kartenausschnitt gezeigt
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,5 @@
Neue Funktionen:
- Auslastungsprognose auch unter Android Auto verfügbar
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,11 @@
Neue Funktionen:
- Adresse oder Koordinaten gedrückt halten um sie in die Zwischenablage zu kopieren
Verbesserungen:
- OpenChargeMap: Links verweisen nun auf die mobile Website
Fehler behoben:
- Favoriten löschen funktionierte nicht bei Rotieren des Bildschirms
- Fehler beim Wechsel zwischen mehreren Fahrzeugen im Preisvergleich
- Android Auto: Adresse wurde nicht korrekt an TomTom GO-Navigation übergeben
- Abstürze behoben

View File

@@ -0,0 +1,5 @@
Bugfixes:
- GoingElectric: filter option "CCS" appeared twice
- Improvements for small displays
- Fixed color of a menu in dark mode
- Fixed crashes

View File

@@ -0,0 +1,8 @@
Improvements:
- New setting for units of measurement
- Adjustments for Android 14
- Android Auto: More detailed descriptions of chargers
- Android Auto: Delete button in filter list
Bugfixes:
- Errors loading realtime data from EnBW

View File

@@ -0,0 +1,5 @@
Improvements:
- When starting the app, the last viewed map area will be shown
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,5 @@
New features:
- Availability prediction also available on Android Auto
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,11 @@
New features:
- Copy address or coordinates using long press
Improvements:
- OpenChargeMap: links now refer to mobile website
Bugfixes:
- Deleting favorites did not work when rotating the screen
- Price comparison: error when switching between different vehicles
- Android Auto: Address was not correctly passed to TomTom GO navigation
- Fixed crashes

View File

@@ -1,6 +1,6 @@
#Sat Aug 06 15:33:46 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,2 +0,0 @@
rootProject.name='EVMap'
include ':app'

2
settings.gradle.kts Normal file
View File

@@ -0,0 +1,2 @@
rootProject.name="EVMap"
include (":app")