mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-27 00:57:45 -05:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca6ff94c1f | ||
|
|
c02c259162 | ||
|
|
a44718ded2 | ||
|
|
4f268f5e83 | ||
|
|
99b4841545 | ||
|
|
7ad7d7da30 | ||
|
|
0e80f2bf82 | ||
|
|
b5a6ceb5f9 | ||
|
|
c655cae405 | ||
|
|
fb2e510220 | ||
|
|
c170270557 | ||
|
|
e7b42e2c19 | ||
|
|
fc85e631c9 | ||
|
|
7192c9ebfa | ||
|
|
c652265ea1 | ||
|
|
0320238dc9 | ||
|
|
e814c088bf | ||
|
|
b60b2d70b9 | ||
|
|
3905656ea7 | ||
|
|
1f88e5fbdd | ||
|
|
646469e9ea | ||
|
|
cce7c69d74 | ||
|
|
bc91c0571b | ||
|
|
a83102a97e | ||
|
|
f52a98540c | ||
|
|
e0d97e7219 | ||
|
|
3bbd20a57e | ||
|
|
3279c5eceb | ||
|
|
03d958ac2c | ||
|
|
b1fd370101 | ||
|
|
bdc96fcd57 | ||
|
|
0d54e17eb4 | ||
|
|
b1d0081fb7 | ||
|
|
1134499532 | ||
|
|
0417ade802 | ||
|
|
8fafabf6a8 | ||
|
|
1b3c35e94f | ||
|
|
23a3adc500 | ||
|
|
16c2dcc938 | ||
|
|
f322974e52 | ||
|
|
50ae2123e9 | ||
|
|
72894399f6 | ||
|
|
77014d754f | ||
|
|
66dbd6426f | ||
|
|
e4127f4a56 | ||
|
|
f9bf8b80f7 | ||
|
|
67eeb47d5f | ||
|
|
3c6a7cd536 | ||
|
|
31e3509369 | ||
|
|
b03f765216 | ||
|
|
9222dec613 | ||
|
|
71c36fbc8f | ||
|
|
830477e664 | ||
|
|
3ce91a9c50 | ||
|
|
a3b2b94b25 | ||
|
|
a7770e1c1b | ||
|
|
fcd51307cb | ||
|
|
ba4a9c29b2 | ||
|
|
3463177ad2 | ||
|
|
09deaf5080 | ||
|
|
23f429bbea | ||
|
|
1184d3b6cc | ||
|
|
c95a60807b | ||
|
|
4b8cf82843 | ||
|
|
f33b9e8117 | ||
|
|
cbc3040807 | ||
|
|
92619ea95e | ||
|
|
a7007284ff | ||
|
|
7fce566052 | ||
|
|
0c44b4b074 | ||
|
|
a652d96f74 | ||
|
|
8a9b3ad948 | ||
|
|
c48f33e265 | ||
|
|
8ba4897026 | ||
|
|
42916d71ca | ||
|
|
5ca7524e8b | ||
|
|
c37f72a26b | ||
|
|
6f0113c50d | ||
|
|
f99ea7ca9e | ||
|
|
788db0c10f | ||
|
|
db2213a50f | ||
|
|
ace4126035 | ||
|
|
d5d6e4f314 | ||
|
|
55999d15e6 | ||
|
|
b61ca609d3 | ||
|
|
b0afad2144 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: johan12345
|
||||
custom: 'https://paypal.me/johan98'
|
||||
custom: ['https://paypal.me/johan98', 'http://ts.la/johan94494']
|
||||
|
||||
1
.github/workflows/apikeys-ci.xml
vendored
1
.github/workflows/apikeys-ci.xml
vendored
@@ -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>
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -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'
|
||||
|
||||
293
app/build.gradle
293
app/build.gradle
@@ -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
353
app/build.gradle.kts
Normal 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
|
||||
}
|
||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
10
app/src/google/res/layout/fragment_donate_header.xml
Normal file
10
app/src/google/res/layout/fragment_donate_header.xml
Normal 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" />
|
||||
16
app/src/google/res/layout/fragment_donate_preview.xml
Normal file
16
app/src/google/res/layout/fragment_donate_preview.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" -> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -116,6 +116,7 @@ class ChargepriceViewModel(
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
vehicle,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
app/src/main/res/layout/activity_oauth_login.xml
Normal file
6
app/src/main/res/layout/activity_oauth_login.xml
Normal 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" />
|
||||
@@ -37,6 +37,8 @@
|
||||
|
||||
<import type="java.time.Duration" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.fronyx.PredictionData" />
|
||||
|
||||
<variable
|
||||
name="charger"
|
||||
type="Resource<ChargeLocation>" />
|
||||
@@ -50,20 +52,8 @@
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="predictionGraph"
|
||||
type="Map<ZonedDateTime, Double>" />
|
||||
|
||||
<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 && !predictionIsPercentage}"
|
||||
app:goneUnless="@{predictionData.predictionGraph != null && !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 && !predictionIsPercentage}"
|
||||
app:goneUnless="@{predictionData.predictionGraph != null && !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 && !predictionIsPercentage}"
|
||||
app:goneUnless="@{predictionData.predictionGraph != null && !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
|
||||
|
||||
@@ -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"
|
||||
|
||||
35
app/src/main/res/layout/fragment_donate_referral.xml
Normal file
35
app/src/main/res/layout/fragment_donate_referral.xml
Normal 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>
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 & 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>
|
||||
@@ -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 d’Android 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">©2020–2023 Johan von Forstner and contributors</string>
|
||||
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
|
||||
</resources>
|
||||
@@ -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 & 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
35
build.gradle
35
build.gradle
@@ -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
35
build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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*
|
||||
|
||||
@@ -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.
|
||||
5
fastlane/metadata/android/de-DE/changelogs/194.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/194.txt
Normal 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
|
||||
8
fastlane/metadata/android/de-DE/changelogs/196.txt
Normal file
8
fastlane/metadata/android/de-DE/changelogs/196.txt
Normal 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
|
||||
5
fastlane/metadata/android/de-DE/changelogs/200.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/200.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Verbesserungen:
|
||||
- Beim Start der App wird nun der zuletzt gesehene Kartenausschnitt gezeigt
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
5
fastlane/metadata/android/de-DE/changelogs/202.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/202.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Neue Funktionen:
|
||||
- Auslastungsprognose auch unter Android Auto verfügbar
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
11
fastlane/metadata/android/de-DE/changelogs/206.txt
Normal file
11
fastlane/metadata/android/de-DE/changelogs/206.txt
Normal 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
|
||||
5
fastlane/metadata/android/en-US/changelogs/194.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/194.txt
Normal 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
|
||||
8
fastlane/metadata/android/en-US/changelogs/196.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/196.txt
Normal 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
|
||||
5
fastlane/metadata/android/en-US/changelogs/200.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/200.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Improvements:
|
||||
- When starting the app, the last viewed map area will be shown
|
||||
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
5
fastlane/metadata/android/en-US/changelogs/202.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/202.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
New features:
|
||||
- Availability prediction also available on Android Auto
|
||||
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
11
fastlane/metadata/android/en-US/changelogs/206.txt
Normal file
11
fastlane/metadata/android/en-US/changelogs/206.txt
Normal 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
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
rootProject.name='EVMap'
|
||||
include ':app'
|
||||
2
settings.gradle.kts
Normal file
2
settings.gradle.kts
Normal file
@@ -0,0 +1,2 @@
|
||||
rootProject.name="EVMap"
|
||||
include (":app")
|
||||
Reference in New Issue
Block a user