Compare commits

...

22 Commits

Author SHA1 Message Date
johan12345
b99e2ea2c8 WIP: replace CustomBottomSheetBehavior 2025-07-12 18:06:51 +02:00
johan12345
d2ae3733d1 Big toolchain update
- Gradle + AGP
- Java 17
- compile/targetSdk 35
- Room & Moshi use KSP, not KAPT
2025-07-12 18:06:27 +02:00
johan12345
72845da4b5 Release 1.9.18 2025-06-14 17:40:09 +02:00
johan12345
51b57433a8 TeslaAvailabilityDetector: Fix nullability bug 2025-06-14 17:36:47 +02:00
johan12345
3202f821d1 always show current location on start, even if we were not following the location before 2025-06-14 17:32:24 +02:00
johan12345
b7e1ff09db FilterScreen: remove unnecessary invalidate() calls
we are already observing filterProfiles
2025-06-13 22:13:05 +02:00
Licaon_Kter
feabf49b8d Remove some non-determinism 2025-05-30 12:03:49 +02:00
johan12345
dcbe4c6325 Release 1.9.17 2025-05-29 00:41:28 +02:00
johan12345
dcff74c125 update TeslaOwnerApi 2025-05-28 23:58:01 +02:00
johan12345
d8f7d77a36 Release 1.9.16 2025-05-17 19:41:36 +02:00
johan12345
d03cf70499 capture (but ignore) clicks on searchResultMarker 2025-05-17 19:36:22 +02:00
johan12345
7a6bebd143 RTL and Arabic locale fixes 2025-05-17 19:24:39 +02:00
johan12345
66d68ca68e disable extendBounds if map is zoomed out far 2025-05-17 18:04:50 +02:00
johan12345
772885a8eb add "zoom in to see all charging stations" snackbar if response is not complete 2025-05-17 17:53:15 +02:00
johan12345
6b07ce012a Rework showLocation function
avoid opening within EVMap itself
2025-05-15 23:15:51 +02:00
johan12345
29dbc202d8 Rework openUrl function
- use preferBrowser only when needed (when opening charger URLs, which might otherwise open in EVMap itself)
- make preferBrowser work even if the default browser does not support Custom Tabs

#313

Cherry-picked from 17efe71
2025-05-15 22:53:19 +02:00
johan12345
cf8371d095 allow Unicode license 2025-05-07 23:38:47 +02:00
johan12345
01cb551cbc Use CarAppService for startActivity instead of CarContext
fixes #375 for startActivity and openUrl

https://issuetracker.google.com/issues/372055514
Warning: You must update to androidx.car.app:1.7.0-alpha01 or later for the permissions dialog to show up on the phone screen when your app is used on a device running Android 14 or higher.
2025-05-07 23:28:54 +02:00
johan12345
45fe297616 Update car app library
fixes #375 for permission request

https://developer.android.com/training/cars/apps#permissions

Warning: You must update to androidx.car.app:1.7.0-alpha01 or later for the permissions dialog to show up on the phone screen when your app is used on a device running Android 14 or higher.
2025-05-07 22:35:02 +02:00
johan12345
32cabefe7d Fix touch targets for privacy policy link on API < 34
https://github.com/material-components/material-components-android/issues/2100#issuecomment-2234437889

fixes #374
2025-04-26 22:35:38 +02:00
johan12345
9ff8329171 Release 1.9.15 2025-03-29 12:15:44 +01:00
johan12345
e9b70a2f00 fix bug in extendBounds
introduced in 890af2ddef
fixes #373
2025-03-29 12:15:33 +01:00
56 changed files with 1927 additions and 852 deletions

View File

@@ -6,6 +6,7 @@ plugins {
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.devtools.ksp").version("2.0.21-1.0.28")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
}
@@ -16,14 +17,18 @@ android {
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 34
compileSdk = 35
minSdk = 21
targetSdk = 34
targetSdk = 35
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 248
versionName = "1.9.14"
versionCode = 256
versionName = "1.9.18"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
val isRunningOnCI = System.getenv("CI") == "true"
@@ -91,18 +96,12 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
@@ -263,10 +262,11 @@ aboutLibraries {
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service", // Google Maps SDK
"provided without support or warranty", // org.json
"Unicode/ICU License", // icu4j
"Unicode/ICU License", "Unicode-3.0", // icu4j
"Bouncy Castle Licence", // bcprov
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
)
excludeFields = arrayOf("generated")
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
@@ -289,19 +289,19 @@ dependencies {
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.8.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.6.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
@@ -315,7 +315,7 @@ dependencies {
implementation("com.github.erfansn:locale-config-x:1.0.1")
// Android Auto
val carAppVersion = "1.4.0"
val carAppVersion = "1.7.0-rc01"
implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
@@ -350,9 +350,9 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// room library
val room_version = "2.6.1"
val room_version = "2.7.1"
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.3.0") {
exclude("com.github.dalgarins", "android-spatialite")
@@ -394,7 +394,7 @@ dependencies {
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")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@@ -0,0 +1,904 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "5dbaaa5adf8cb9b6e8a8314bb7766447",
"entities": [
{
"tableName": "ChargeLocation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "coordinates",
"columnName": "coordinates",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "chargepoints",
"columnName": "chargepoints",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "network",
"columnName": "network",
"affinity": "TEXT"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "editUrl",
"columnName": "editUrl",
"affinity": "TEXT"
},
{
"fieldPath": "verified",
"columnName": "verified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "barrierFree",
"columnName": "barrierFree",
"affinity": "INTEGER"
},
{
"fieldPath": "operator",
"columnName": "operator",
"affinity": "TEXT"
},
{
"fieldPath": "generalInformation",
"columnName": "generalInformation",
"affinity": "TEXT"
},
{
"fieldPath": "amenities",
"columnName": "amenities",
"affinity": "TEXT"
},
{
"fieldPath": "locationDescription",
"columnName": "locationDescription",
"affinity": "TEXT"
},
{
"fieldPath": "photos",
"columnName": "photos",
"affinity": "TEXT"
},
{
"fieldPath": "chargecards",
"columnName": "chargecards",
"affinity": "TEXT"
},
{
"fieldPath": "license",
"columnName": "license",
"affinity": "TEXT"
},
{
"fieldPath": "networkUrl",
"columnName": "networkUrl",
"affinity": "TEXT"
},
{
"fieldPath": "chargerUrl",
"columnName": "chargerUrl",
"affinity": "TEXT"
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address.city",
"columnName": "city",
"affinity": "TEXT"
},
{
"fieldPath": "address.country",
"columnName": "country",
"affinity": "TEXT"
},
{
"fieldPath": "address.postcode",
"columnName": "postcode",
"affinity": "TEXT"
},
{
"fieldPath": "address.street",
"columnName": "street",
"affinity": "TEXT"
},
{
"fieldPath": "faultReport.created",
"columnName": "fault_report_created",
"affinity": "INTEGER"
},
{
"fieldPath": "faultReport.description",
"columnName": "fault_report_description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.twentyfourSeven",
"columnName": "twentyfourSeven",
"affinity": "INTEGER"
},
{
"fieldPath": "openinghours.description",
"columnName": "description",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.start",
"columnName": "mostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.monday.end",
"columnName": "moend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.start",
"columnName": "tustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.tuesday.end",
"columnName": "tuend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.start",
"columnName": "westart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.wednesday.end",
"columnName": "weend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.start",
"columnName": "thstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.thursday.end",
"columnName": "thend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.start",
"columnName": "frstart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.friday.end",
"columnName": "frend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.start",
"columnName": "sastart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.saturday.end",
"columnName": "saend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.start",
"columnName": "sustart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.sunday.end",
"columnName": "suend",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.start",
"columnName": "hostart",
"affinity": "TEXT"
},
{
"fieldPath": "openinghours.days.holiday.end",
"columnName": "hoend",
"affinity": "TEXT"
},
{
"fieldPath": "cost.freecharging",
"columnName": "freecharging",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.freeparking",
"columnName": "freeparking",
"affinity": "INTEGER"
},
{
"fieldPath": "cost.descriptionShort",
"columnName": "descriptionShort",
"affinity": "TEXT"
},
{
"fieldPath": "cost.descriptionLong",
"columnName": "descriptionLong",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.country",
"columnName": "chargepricecountry",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.network",
"columnName": "chargepricenetwork",
"affinity": "TEXT"
},
{
"fieldPath": "chargepriceData.plugTypes",
"columnName": "chargepriceplugTypes",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "Favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "favoriteId",
"columnName": "favoriteId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerId",
"columnName": "chargerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chargerDataSource",
"columnName": "chargerDataSource",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"favoriteId"
]
},
"indices": [
{
"name": "index_Favorite_chargerId_chargerDataSource",
"unique": false,
"columnNames": [
"chargerId",
"chargerDataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
}
],
"foreignKeys": [
{
"table": "ChargeLocation",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"chargerId",
"chargerDataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "BooleanFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_BooleanFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "MultipleChoiceFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "values",
"columnName": "values",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "all",
"columnName": "all",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "SliderFilterValue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profile",
"columnName": "profile",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key",
"profile",
"dataSource"
]
},
"indices": [
{
"name": "index_SliderFilterValue_profile_dataSource",
"unique": false,
"columnNames": [
"profile",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
}
],
"foreignKeys": [
{
"table": "FilterProfile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"profile",
"dataSource"
],
"referencedColumns": [
"id",
"dataSource"
]
}
]
},
{
"tableName": "FilterProfile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"dataSource",
"id"
]
},
"indices": [
{
"name": "index_FilterProfile_dataSource_name",
"unique": true,
"columnNames": [
"dataSource",
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
}
]
},
{
"tableName": "RecentAutocompletePlace",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "primaryText",
"columnName": "primaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondaryText",
"columnName": "secondaryText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "latLng",
"columnName": "latLng",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "viewport",
"columnName": "viewport",
"affinity": "TEXT"
},
{
"fieldPath": "types",
"columnName": "types",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"dataSource"
]
}
},
{
"tableName": "GEPlug",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GENetwork",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "GEChargeCard",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMConnectionType",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formalName",
"columnName": "formalName",
"affinity": "TEXT"
},
{
"fieldPath": "discontinued",
"columnName": "discontinued",
"affinity": "INTEGER"
},
{
"fieldPath": "obsolete",
"columnName": "obsolete",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMCountry",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isoCode",
"columnName": "isoCode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "continentCode",
"columnName": "continentCode",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "OCMOperator",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "websiteUrl",
"columnName": "websiteUrl",
"affinity": "TEXT"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contactEmail",
"columnName": "contactEmail",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone1",
"columnName": "contactTelephone1",
"affinity": "TEXT"
},
{
"fieldPath": "contactTelephone2",
"columnName": "contactTelephone2",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "SavedRegion",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "region",
"columnName": "region",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "dataSource",
"columnName": "dataSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeRetrieved",
"columnName": "timeRetrieved",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "filters",
"columnName": "filters",
"affinity": "TEXT"
},
{
"fieldPath": "isDetailed",
"columnName": "isDetailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SavedRegion_filters_dataSource",
"unique": false,
"columnNames": [
"filters",
"dataSource"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5dbaaa5adf8cb9b6e8a8314bb7766447')"
]
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap (debug)</string>
<string name="app_name">EVMap</string>
</resources>

View File

@@ -24,6 +24,10 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />
@@ -41,7 +45,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true">
<meta-data
android:name="com.mapbox.ACCESS_TOKEN"

View File

@@ -3,6 +3,8 @@ package net.vonforst.evmap
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -11,7 +13,6 @@ import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen
@@ -256,9 +257,24 @@ class MapsActivity : AppCompatActivity(),
Uri.encode(charger.name)
})"
)
if (intent.resolveActivity(packageManager) != null) {
val resolveInfo =
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg =
resolveInfo?.activityInfo?.packageName.takeIf { it != "android" && it != packageName }
if (pkg == null) {
// There is no default maps app or EVMap itself is the current default, fall back to app chooser
val chooserIntent = Intent.createChooser(intent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
startActivity(chooserIntent)
return
}
intent.setPackage(pkg)
try {
startActivity(intent)
} else {
} catch (e: ActivityNotFoundException) {
Snackbar.make(
rootView,
R.string.no_maps_app_found,
@@ -268,7 +284,6 @@ class MapsActivity : AppCompatActivity(),
}
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -276,13 +291,46 @@ class MapsActivity : AppCompatActivity(),
.build()
)
.build()
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
if (preferBrowser) intent.intent.setPackage(pkg)
val uri = Uri.parse(url)
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
if (preferBrowser) {
// EVMap may be set as default app for this link, but we want to open it in a browser
// try to find default web browser
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
val resolveInfo =
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg = resolveInfo?.activityInfo?.packageName.takeIf { it != "android" }
if (pkg == null) {
// There is no default browser, fall back to app chooser
val chooserIntent = Intent.createChooser(viewIntent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
val targets: List<ResolveInfo> = packageManager.queryIntentActivities(
viewIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
// add missing browsers (if EVMap is already set as default, Android might not find other browsers with the specific intent)
val browsers = packageManager.queryIntentActivities(
browserIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
val extraIntents = browsers.filter { browser ->
targets.find { it.activityInfo.packageName == browser.activityInfo.packageName } == null
}.map { browser ->
Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(browser.activityInfo.packageName)
}
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toTypedArray())
startActivity(chooserIntent)
return
}
intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, Uri.parse(url))
intent.launchUrl(this, uri)
} catch (e: ActivityNotFoundException) {
Snackbar.make(
rootView,

View File

@@ -16,6 +16,8 @@ import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.text.TextUtils
import android.text.style.StyleSpan
import android.view.View
import android.view.ViewTreeObserver
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Currency
import java.util.Locale
@@ -142,4 +144,12 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
}
}
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
inline fun View.waitForLayout(crossinline f: () -> Unit) =
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})

View File

@@ -5,7 +5,6 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -14,6 +13,7 @@ import coil.load
import coil.memory.MemoryCache
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.waitForLayout
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
@@ -39,12 +39,9 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
val item = getItem(position)
if (holder.view.height == 0) {
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
loadImage(item, holder)
}
})
holder.view.waitForLayout {
loadImage(item, holder)
}
} else {
loadImage(item, holder)
}

View File

@@ -2,7 +2,6 @@ package net.vonforst.evmap.api.availability
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
import net.vonforst.evmap.model.ChargeLocation
@@ -59,7 +58,7 @@ class TeslaOwnerAvailabilityDetector(
val details = api.getChargingSiteInformation(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text),
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
)
)

View File

@@ -58,7 +58,7 @@ data class Rates(
@JsonClass(generateAdapter = true)
data class Pricebook(
val charging: PricebookDetails,
val parking: PricebookDetails,
val parking: PricebookDetails?,
val priceBookID: Long?
)

View File

@@ -16,7 +16,6 @@ import retrofit2.http.POST
import retrofit2.http.Query
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.LocalTime
interface TeslaAuthenticationApi {
@POST("oauth2/v3/token")
@@ -131,8 +130,8 @@ interface TeslaOwnerApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
@@ -173,7 +172,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites",
override val query: String =
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n locationGUID\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n teslaExclusive\n amenities\n chargingAccessibility\n ownerType\n isThirdPartySite\n usabilityArchetype\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isMagicDockSupportedSite\n hasParkingBenefit\n hasTou\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -202,7 +201,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation",
override val query: String =
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n upsellingBanner(vehicleMakeType: \$vehicleMakeType) {\n header\n caption\n backgroundImageUrl\n routeName\n }\n nacsOnlyAssets {\n banner {\n header\n caption\n link\n }\n disclaimer {\n text\n sheetTitle\n sheetContent\n }\n }\n enableChargingSiteReportIssue\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n locationGUID\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isThirdPartySite\n isMagicDockSupportedSite\n trtId {\n value\n }\n siteDisclaimer\n chargingAccessibility\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isCanvasSite\n ownerDisclaimer\n chargingFeesDisclaimer {\n title\n description\n }\n idleFeesDisclaimer {\n title\n description\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n stateOfCharge\n chargerDisabled\n }\n waitEstimateBucket\n currentCongestion\n usabilityArchetype\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n dynamicRates {\n enabled\n }\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n congestion {\n ...ChargingUserRateFragment\n }\n service {\n ...ChargingUserRateFragment\n }\n electricity {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n stateOfCharge\n congestionGracePeriodSecs\n congestionPercent\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -217,11 +216,11 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
)
enum class ChargingSiteIdentifierType {
SITE_ID
SITE_ID, LOCATION_GUID
}
enum class VehicleMakeType {
@@ -242,7 +241,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?,
val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?,
@@ -251,7 +249,8 @@ interface TeslaChargingOwnershipGraphQlApi {
val id: Text,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val totalStalls: Value<Int>
val totalStalls: Value<Int>,
val locationGUID: String
// TODO: siteType, accessType
)
@@ -274,7 +273,6 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?,
val currentCongestion: Double,
@@ -373,8 +371,8 @@ interface TeslaChargingOwnershipGraphQlApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)

View File

@@ -162,7 +162,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}
if (!prefs.privacyAccepted) {
screens.add(
AcceptPrivacyScreen(carContext)
AcceptPrivacyScreen(carContext, this)
)
}
handleACRAIntent(intent)?.let {

View File

@@ -3,13 +3,22 @@ package net.vonforst.evmap.auto
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.Model
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import jsonapi.Meta
import jsonapi.Relationship
import jsonapi.Relationships
@@ -18,7 +27,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
@@ -32,7 +50,9 @@ import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
@ExperimentalCarApi
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
@@ -70,7 +90,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower()} ${
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
carContext.getString(
R.string.chargeprice_stats,
meta.energy,
@@ -130,7 +150,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
)
).build()
).setOnClickListener {
openUrl(carContext, ChargepriceApi.getPoiUrl(charger))
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
}.build()
).build()
)

View File

@@ -16,6 +16,7 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.HostException
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
@@ -34,6 +35,7 @@ import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import coil.imageLoader
import coil.request.ImageRequest
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -76,7 +78,12 @@ import kotlin.math.roundToInt
private const val TAG = "ChargerDetailScreen"
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
@ExperimentalCarApi
class ChargerDetailScreen(
ctx: CarContext,
val chargerSparse: ChargeLocation,
val session: EVMapSession
) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
private var availability: ChargeLocationStatus? = null
@@ -153,14 +160,20 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
if (prefs.chargepriceNativeIntegration) {
screenManager.push(ChargepriceScreen(carContext, charger))
screenManager.push(
ChargepriceScreen(
carContext,
session,
charger
)
)
} else {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger))
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
carContext.startActivity(intent)
session.cas.startActivity(intent)
}
}
.build())
@@ -179,12 +192,12 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
Action.Builder()
.setTitle(carContext.getString(R.string.open_in_app))
.setOnClickListener(ParkedOnlyOnClickListener.create {
val intent = Intent(carContext, MapsActivity::class.java)
val intent = Intent(session.cas, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
.putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
.putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
carContext.startActivity(intent)
session.cas.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
@@ -366,7 +379,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
var text = formatTeslaPricing(teslaPricing, carContext) as CharSequence
formatTeslaParkingFee(teslaPricing, carContext)?.let { text += "\n\n" + it }
addText(text)
} ?: {
} ?: run {
addText(carContext.getString(if (prediction != null) R.string.auto_no_data else R.string.loading))
}
}.build())
@@ -511,7 +524,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
} else {
append(nameForPlugType(carContext.stringProvider(), cp.type))
}
cp.formatPower()?.let {
cp.formatPower(carContext.currentOrDefaultLocale)?.let {
append(" ")
append(it)
}

View File

@@ -9,14 +9,36 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.BooleanFilterValue
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.MultipleChoiceFilterValue
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.SliderFilterValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -232,7 +254,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
}
}
}.build())
@@ -349,7 +370,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
),
CarToast.LENGTH_SHORT
).show()
invalidate()
screenManager.pop()
}
}
@@ -381,7 +401,6 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
}
if (!saveSuccess) return@pushForResult
}
invalidate()
}
.build()
)

View File

@@ -15,14 +15,34 @@ import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.CarLocation
import androidx.car.app.model.CarText
import androidx.car.app.model.DistanceSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.Metadata
import androidx.car.app.model.OnContentRefreshListener
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceListMapTemplate
import androidx.car.app.model.PlaceMarker
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityRepository
@@ -386,7 +406,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
setOnClickListener {
screenManager.push(ChargerDetailScreen(carContext, charger))
screenManager.push(ChargerDetailScreen(carContext, charger, session))
session.mapScreen = null
}
}.build()
@@ -472,13 +492,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
zoom = 16f,
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data?.items.isNullOrEmpty() else response.data == null) {
loadingError = true
this@MapScreen.chargers = null
invalidate()
return@launch
}
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers = response.data?.items?.filterIsInstance<ChargeLocation>()
if (prefs.placeSearchResultAndroidAutoName == null) {
chargers = headingFilter(
chargers,

View File

@@ -79,7 +79,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
setBrowsable(true)
setOnClickListener {
screenManager.push(DataSettingsScreen(carContext))
screenManager.push(DataSettingsScreen(carContext, session))
}
}.build())
addItem(Row.Builder().apply {
@@ -144,7 +144,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(AboutScreen(carContext))
screenManager.push(AboutScreen(carContext, session))
}
.build()
)
@@ -153,7 +153,8 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
}
}
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
val db = AppDatabase.getInstance(ctx)
@@ -279,7 +280,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
carContext.startActivity(intent)
session.cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
CarToast.makeText(
@@ -752,7 +753,8 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
}
}
class AboutScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class AboutScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
var developerOptionsCounter = 0
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
@@ -797,7 +799,11 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setTitle(carContext.getString(R.string.faq))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.faq_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.faq_link)
)
}).build()
)
addItem(
@@ -808,12 +814,16 @@ 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.donate_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.donate_link)
)
} else {
val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_DONATE, true)
carContext.startActivity(intent)
session.cas.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
@@ -829,7 +839,11 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.addText(carContext.getString(R.string.mastodon_handle))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.mastodon_url))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.mastodon_url)
)
}).build()
)
if (maxRows > 8) {
@@ -839,7 +853,11 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.addText(carContext.getString(R.string.twitter_handle))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.twitter_url))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.twitter_url)
)
}).build()
)
}
@@ -849,7 +867,7 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext,
carContext, session.cas,
carContext.getString(R.string.goingelectric_forum_url)
)
}).build()
@@ -862,7 +880,7 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(
carContext,
carContext, session.cas,
carContext.getString(R.string.tff_forum_url)
)
}).build()
@@ -874,14 +892,18 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
.setTitle(carContext.getString(R.string.github_link_title))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.github_link))
openUrl(carContext, session.cas, carContext.getString(R.string.github_link))
}).build()
)
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setBrowsable(true)
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
openUrl(
carContext,
session.cas,
carContext.getString(R.string.privacy_link)
)
}).build()
)
}.build(), carContext.getString(R.string.other)))
@@ -889,7 +911,8 @@ class AboutScreen(ctx: CarContext) : Screen(ctx) {
}
}
class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
@ExperimentalCarApi
class AcceptPrivacyScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
val textWithoutLink = HtmlCompat.fromHtml(
@@ -910,7 +933,7 @@ class AcceptPrivacyScreen(ctx: CarContext) : Screen(ctx) {
addAction(Action.Builder()
.setTitle(carContext.getString(R.string.privacy))
.setOnClickListener(ParkedOnlyOnClickListener.create {
openUrl(carContext, carContext.getString(R.string.privacy_link))
openUrl(carContext, session.cas, carContext.getString(R.string.privacy_link))
}).build()
)
}.build()

View File

@@ -12,6 +12,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
@@ -201,7 +202,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")
return info.versionName!!.split(".")
}
fun supportsCarApiLevel3(ctx: CarContext): Boolean {
@@ -221,13 +222,14 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
return true
}
fun openUrl(carContext: CarContext, url: String) {
@ExperimentalCarApi
fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(
ContextCompat.getColor(
carContext,
cas,
R.color.colorPrimary
)
)
@@ -237,7 +239,7 @@ fun openUrl(carContext: CarContext, url: String) {
intent.data = Uri.parse(url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
cas.startActivity(intent)
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(

View File

@@ -24,6 +24,7 @@ import android.widget.AdapterView
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
@@ -35,7 +36,6 @@ import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
@@ -62,6 +62,11 @@ import com.car2go.maps.model.BitmapDescriptor
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.Marker
import com.car2go.maps.model.MarkerOptions
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_SETTLING
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion
@@ -69,14 +74,6 @@ import com.google.android.material.transition.MaterialContainerTransform
import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS
import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.from
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
@@ -90,6 +87,7 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
@@ -146,8 +144,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
private var map: AnyMap? = null
private lateinit var locationEngine: LocationEngine
private var requestingLocationUpdates = false
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
private lateinit var detailsDialog: ConnectorDetailsDialog
private lateinit var prefs: PreferenceDataSource
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
@@ -155,6 +152,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
private var searchResultMarker: Marker? = null
private var searchResultIcon: BitmapDescriptor? = null
private var connectionErrorSnackbar: Snackbar? = null
private var zoomInSnackbar: Snackbar? = null
private var previousChargepointIds: Set<Long>? = null
private var mapTopPadding: Int = 0
private var popupMenu: PopupMenu? = null
@@ -163,6 +161,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
private lateinit var chargerIconGenerator: ChargerIconGenerator
private lateinit var animator: MarkerAnimator
private lateinit var favToggle: MenuItem
private val bottomSheetBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val state = bottomSheetBehavior.state
when (state) {
STATE_COLLAPSED -> vm.chargerSparse.value = null
STATE_HIDDEN -> return
else -> if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
bottomSheetBehavior.cancelBackProgress()
}
override fun handleOnBackStarted(backEvent: BackEventCompat) {
bottomSheetBehavior.startBackProgress(backEvent)
}
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
bottomSheetBehavior.updateBackProgress(backEvent)
}
override fun handleOnBackCancelled() {
bottomSheetBehavior.cancelBackProgress()
}
}
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val value = vm.layersMenuOpen.value
@@ -179,18 +206,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
if (binding.search.hasFocus()) {
removeSearchFocus()
return
}
val state = bottomSheetBehavior.state
when (state) {
STATE_COLLAPSED -> vm.chargerSparse.value = null
STATE_HIDDEN -> vm.searchResult.value = null
else -> if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
vm.searchResult.value = null
}
}
@@ -243,7 +262,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
searchResultIcon = null
}
binding.detailAppBar.toolbar.popupTheme =
binding.detailView.toolbar.popupTheme =
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
ViewCompat.setOnApplyWindowInsetsListener(
@@ -252,9 +271,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
ViewCompat.onApplyWindowInsets(binding.root, insets)
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
/*binding.detailView.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemWindowInsetTop
}
}*/
val insetsBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetsBottom
// margin of layers button: status bar height + toolbar height + margin
val density = resources.displayMetrics.density
@@ -287,6 +309,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
viewLifecycleOwner,
backPressedCallback
)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
bottomSheetBackPressedCallback
)
return binding.root
}
@@ -308,22 +334,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
mapFragment!!.getMapAsync(this)
bottomSheetBehavior = from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
bottomSheetBehavior = BottomSheetBehavior.from(binding.detailView.root)
//detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav)
binding.detailView.toolbar.inflateMenu(R.menu.detail)
favToggle = binding.detailView.toolbar.menu.findItem(R.id.menu_fav)
vm.apiName.observe(viewLifecycleOwner) {
binding.detailAppBar.toolbar.menu.findItem(R.id.menu_edit).title =
binding.detailView.toolbar.menu.findItem(R.id.menu_edit).title =
getString(R.string.edit_at_datasource, it)
}
binding.detailView.topPart.doOnNextLayout {
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
}
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
bottomSheetBehavior.skipCollapsed = !bottomSheetCollapsible
bottomSheetBehavior.state = STATE_HIDDEN
binding.detailView.connectorDetails
setupObservers()
@@ -438,7 +462,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
binding.detailView.sourceButton.setOnClickListener {
val charger = vm.charger.value?.data
if (charger != null) {
(activity as? MapsActivity)?.openUrl(charger.url, binding.root)
(activity as? MapsActivity)?.openUrl(charger.url, binding.root, true)
}
}
binding.detailView.btnChargeprice.setOnClickListener {
@@ -453,8 +477,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
} else {
(activity as? MapsActivity)?.openUrl(
ChargepriceApi.getPoiUrl(charger),
binding.root,
false
binding.root
)
}
}
@@ -477,7 +500,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
.show()
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = STATE_ANCHOR_POINT
bottomSheetBehavior.state = STATE_HALF_EXPANDED
}
binding.detailView.topPart.setOnLongClickListener {
val charger = vm.charger.value?.data ?: return@setOnLongClickListener false
@@ -485,14 +508,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
return@setOnLongClickListener true
}
setupSearchAutocomplete()
binding.detailAppBar.toolbar.setNavigationOnClickListener {
binding.detailView.toolbar.setNavigationOnClickListener {
if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
binding.detailView.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_fav -> {
toggleFavorite()
@@ -508,7 +531,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
R.id.menu_edit -> {
val charger = vm.charger.value?.data
if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root)
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
if (vm.apiId.value == "goingelectric") {
// instructions specific to GoingElectric
Toast.makeText(
@@ -653,7 +676,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
private fun setupObservers() {
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetCallback() {
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (bottomSheetBehavior.state == STATE_HIDDEN) {
map?.setPadding(0, mapTopPadding, 0, 0)
@@ -670,7 +693,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
override fun onStateChanged(bottomSheet: View, newState: Int) {
vm.bottomSheetState.value = newState
updateBackPressedCallback()
bottomSheetBackPressedCallback.isEnabled = newState != STATE_HIDDEN
if (vm.layersMenuOpen.value!! && newState !in listOf(
STATE_SETTLING,
@@ -682,7 +705,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
}
if (vm.selectedChargepoint.value != null && newState in listOf(
STATE_ANCHOR_POINT, STATE_COLLAPSED
STATE_HALF_EXPANDED, STATE_COLLAPSED
)
) {
closeConnectorDetailsDialog()
@@ -692,13 +715,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
})
vm.chargerSparse.observe(viewLifecycleOwner) {
if (it != null) {
if (vm.bottomSheetState.value != STATE_ANCHOR_POINT) {
if (vm.bottomSheetState.value != STATE_HALF_EXPANDED) {
bottomSheetBehavior.state =
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_HALF_EXPANDED
}
removeSearchFocus()
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
//detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle()
highlightMarker(it)
} else {
@@ -709,12 +732,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
val chargepoints = res.data
if (chargepoints != null) {
updateMap(chargepoints)
updateMap(chargepoints.items)
}
val view = view ?: return@Observer
when (res.status) {
Status.ERROR -> {
val view = view ?: return@Observer
zoomInSnackbar?.dismiss()
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
@@ -726,13 +749,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
}
Status.SUCCESS -> {
connectionErrorSnackbar?.dismiss()
if (res.data != null && !res.data.isComplete) {
zoomInSnackbar?.dismiss()
zoomInSnackbar = Snackbar
.make(view, R.string.zoom_in_to_see_more, Snackbar.LENGTH_INDEFINITE)
zoomInSnackbar!!.show()
}
}
Status.LOADING -> {
zoomInSnackbar?.dismiss()
}
}
})
vm.useMiniMarkers.observe(viewLifecycleOwner) {
vm.chargepoints.value?.data?.let { updateMap(it) }
vm.chargepoints.value?.data?.let { updateMap(it.items) }
}
vm.favorites.observe(viewLifecycleOwner) {
updateFavoriteToggle()
@@ -758,7 +788,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
if (it != null) {
detailsDialog.setData(it, vm.availability.value?.data)
}
updateBackPressedCallback()
}
updateBackPressedCallback()
@@ -799,12 +828,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
}
private fun updateBackPressedCallback() {
backPressedCallback.isEnabled =
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|| vm.searchResult.value != null
backPressedCallback.isEnabled = vm.searchResult.value != null
|| (vm.layersMenuOpen.value ?: false)
|| binding.search.hasFocus()
|| vm.selectedChargepoint.value != null
}
private fun unhighlightAllMarkers() {
@@ -927,7 +953,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
(activity as? MapsActivity)?.showLocation(charger, binding.root)
}
R.drawable.ic_fault_report -> {
(activity as? MapsActivity)?.openUrl(charger.url, binding.root)
(activity as? MapsActivity)?.openUrl(
charger.url,
binding.root,
true
)
}
R.drawable.ic_payment -> {
@@ -1156,6 +1186,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
)
true
}
searchResultMarker -> true
else -> false
}
@@ -1163,6 +1194,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
map.setOnMapClickListener {
if (backPressedCallback.isEnabled) {
backPressedCallback.handleOnBackPressed()
} else if (bottomSheetBackPressedCallback.isEnabled) {
bottomSheetBackPressedCallback.handleOnBackPressed()
}
}
map.setMapType(vm.mapType.value)
@@ -1217,10 +1250,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
// show charger detail after chargers were loaded
vm.chargepoints.observe(
viewLifecycleOwner,
object : Observer<Resource<List<ChargepointListItem>>> {
override fun onChanged(value: Resource<List<ChargepointListItem>>) {
object : Observer<Resource<ChargepointList>> {
override fun onChanged(value: Resource<ChargepointList>) {
if (value.data == null) return
for (item in value.data) {
for (item in value.data.items) {
if (item is ChargeLocation && item.id == chargerId) {
vm.chargerSparse.value = item
vm.chargepoints.removeObserver(this)
@@ -1240,9 +1273,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
binding.search.requestFocus()
binding.search.setSelection(locationName.length)
}
if (context.checkAnyLocationPermission() && prefs.currentMapMyLocationEnabled) {
enableLocation(!positionSet, false)
positionSet = true
if (context.checkAnyLocationPermission()) {
if (prefs.currentMapMyLocationEnabled && !positionSet) {
enableLocation(true, false)
positionSet = true
} else {
enableLocation(false, false)
}
}
if (!positionSet) {
// use position saved in preferences, fall back to default (Europe)

View File

@@ -6,8 +6,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -15,15 +13,21 @@ import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.*
import net.vonforst.evmap.databinding.FragmentOnboardingAndroidAutoBinding
import net.vonforst.evmap.databinding.FragmentOnboardingBinding
import net.vonforst.evmap.databinding.FragmentOnboardingDataSourceBinding
import net.vonforst.evmap.databinding.FragmentOnboardingIconsBinding
import net.vonforst.evmap.databinding.FragmentOnboardingWelcomeBinding
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.waitForLayout
class OnboardingFragment : Fragment() {
private lateinit var binding: FragmentOnboardingBinding
@@ -59,7 +63,6 @@ class OnboardingFragment : Fragment() {
}
override fun onPageSelected(position: Int) {
binding.pageIndicatorView.selection = position
binding.forward?.visibility =
if (position == adapter.itemCount - 1) View.INVISIBLE else View.VISIBLE
binding.backward?.visibility = if (position == 0) View.INVISIBLE else View.VISIBLE
@@ -76,9 +79,13 @@ class OnboardingFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (prefs.welcomeDialogShown) {
// skip to last page for selecting data source or accepting the privacy policy
binding.viewPager.currentItem = adapter.itemCount - 1
binding.root.waitForLayout {
binding.viewPager.currentItem = if (prefs.welcomeDialogShown) {
// skip to last page for selecting data source or accepting the privacy policy
adapter.itemCount - 1
} else {
0
}
}
}
@@ -234,7 +241,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
), HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.cbAcceptPrivacy.linksClickable = true
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethod.getInstance()
binding.cbAcceptPrivacy.movementMethod = LinkMovementMethodCompat.getInstance()
binding.btnGetStarted.visibility = View.INVISIBLE
for (rb in listOf(

View File

@@ -18,6 +18,7 @@ import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.abs
sealed class ChargepointListItem
@@ -140,9 +141,9 @@ data class ChargeLocation(
val totalChargepoints: Int
get() = chargepoints.sumOf { it.count }
fun formatChargepoints(sp: StringProvider): String {
fun formatChargepoints(sp: StringProvider, locale: Locale): String {
return chargepointsMerged.joinToString(" · ") {
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}"
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower(locale)}"
}
}
}
@@ -413,12 +414,12 @@ data class Chargepoint(
* If chargepoint power is defined, format it into a string.
* Otherwise, return null.
*/
fun formatPower(): String? {
fun formatPower(locale: Locale): String? {
if (power == null) return null
val powerFmt = if (abs(power - power.toInt()) < 0.1) {
"%.0f".format(power)
"%.0f".format(locale, power)
} else {
"%.1f".format(power)
"%.1f".format(locale, power)
}
return "$powerFmt kW"
}

View File

@@ -146,7 +146,7 @@ class ChargeLocationsRepository(
zoom: Float,
filters: FilterValues?,
overrideCache: Boolean = false
): LiveData<Resource<List<ChargepointListItem>>> {
): LiveData<Resource<ChargepointList>> {
if (bounds.crossesAntimeridian()) {
val (a, b) = bounds.splitAtAntimeridian()
val liveDataA = getChargepoints(a, zoom, filters, overrideCache)
@@ -168,7 +168,7 @@ class ChargeLocationsRepository(
)
} else {
queryWithFilters(api, filters, bounds)
}.map { applyLocalClustering(it, zoom) }
}.map { ChargepointList(applyLocalClustering(it, zoom), true) }
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
@@ -219,19 +219,22 @@ class ChargeLocationsRepository(
}
private fun combineLiveData(
liveDataA: LiveData<Resource<List<ChargepointListItem>>>,
liveDataB: LiveData<Resource<List<ChargepointListItem>>>
) = MediatorLiveData<Resource<List<ChargepointListItem>>>().apply {
liveDataA: LiveData<Resource<ChargepointList>>,
liveDataB: LiveData<Resource<ChargepointList>>
) = MediatorLiveData<Resource<ChargepointList>>().apply {
listOf(liveDataA, liveDataB).forEach {
addSource(it) {
val valA = liveDataA.value
val valB = liveDataB.value
val combinedList = if (valA?.data != null && valB?.data != null) {
valA.data + valB.data
ChargepointList(
valA.data.items + valB.data.items,
valA.data.isComplete && valB.data.isComplete
)
} else if (valA?.data != null) {
valA.data
ChargepointList(valA.data.items, false)
} else if (valB?.data != null) {
valB.data
ChargepointList(valB.data.items, false)
} else null
if (valA?.status == Status.SUCCESS && valB?.status == Status.SUCCESS) {
Resource.success(combinedList)
@@ -249,7 +252,7 @@ class ChargeLocationsRepository(
radius: Int,
zoom: Float,
filters: FilterValues?
): LiveData<Resource<List<ChargepointListItem>>> {
): LiveData<Resource<ChargepointList>> {
val api = api.value!!
val radiusMeters = radius.toDouble() * 1000
@@ -263,7 +266,7 @@ class ChargeLocationsRepository(
)
} else {
queryWithFilters(api, filters, location, radiusMeters)
}.map { applyLocalClustering(it, zoom) }
}.map { ChargepointList(applyLocalClustering(it, zoom), true) }
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
@@ -313,18 +316,18 @@ class ChargeLocationsRepository(
private fun applyLocalClustering(
result: Resource<ChargepointList>,
zoom: Float
): Resource<List<ChargepointListItem>> {
): Resource<ChargepointList> {
val list = result.data ?: return Resource(result.status, null, result.message)
val chargers = list.items.filterIsInstance<ChargeLocation>()
if (chargers.size != list.items.size) return Resource(
result.status,
list.items,
list,
result.message
) // list already contains clusters
val clustered = applyLocalClustering(chargers, zoom)
return Resource(result.status, clustered, result.message)
return Resource(result.status, ChargepointList(clustered, list.isComplete), result.message)
}
private fun applyLocalClustering(

View File

@@ -117,6 +117,16 @@ class Converters {
return stringSetAdapter.fromJson(value)
}
@TypeConverter
fun fromStringMutableSet(value: MutableSet<String>?): String {
return stringSetAdapter.toJson(value)
}
@TypeConverter
fun toStringMutableSet(value: String): MutableSet<String>? {
return stringSetAdapter.fromJson(value)?.toMutableSet()
}
@TypeConverter
fun fromStringList(value: List<String>?): String {
return stringListAdapter.toJson(value)

View File

@@ -1,112 +0,0 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.widget.NestedScrollView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
FloatingActionButton.Behavior(context, attrs) {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(
coordinatorLayout,
child,
directTargetChild,
target,
axes,
type
)
}
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
if (dependency is NestedScrollView) {
try {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
behavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
onDependentViewChanged(parent, child, dependency)
}
})
return true
} catch (e: IllegalArgumentException) {
}
}
return false
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
}
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
if (child.tag as? Boolean != false) child.show()
}
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED -> {
if (child.tag as? Boolean != false) child.show()
}
else -> {
child.hide()
}
}
return false
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
type,
consumed
)
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide()
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show()
}
}
}

View File

@@ -6,8 +6,8 @@ import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
@@ -45,9 +45,9 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
): Boolean {
if (dependency is NestedScrollView) {
try {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
val behavior = BottomSheetBehavior.from<View>(dependency)
behavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
@@ -68,12 +68,13 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
child: FloatingActionButton,
dependency: View
): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from(dependency)
val behavior = BottomSheetBehavior.from(dependency)
when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
BottomSheetBehavior.STATE_SETTLING -> {
}
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
BottomSheetBehavior.STATE_HIDDEN -> {
if (!hidden) child.show()
}
else -> {

View File

@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.text.BidiFormatter
import androidx.core.content.ContextCompat
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
@@ -154,11 +155,13 @@ private fun dms(value: Double, lon: Boolean): String {
}
fun Coordinate.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng)
return BidiFormatter.getInstance()
.unicodeWrap("%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng))
}
fun Location.formatDecimal(accuracy: Int = 6): String {
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude)
return BidiFormatter.getInstance()
.unicodeWrap("%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude))
}
fun LatLngBounds.normalize() = LatLngBounds(

View File

@@ -15,11 +15,15 @@ import androidx.lifecycle.viewModelScope
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.tesla.Pricing
@@ -32,7 +36,6 @@ import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.Favorite
@@ -92,12 +95,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val bottomSheetExpanded = MediatorLiveData<Boolean>().apply {
addSource(bottomSheetState) {
when (it) {
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
STATE_COLLAPSED,
STATE_HIDDEN -> {
value = false
}
BottomSheetBehaviorGoogleMapsLike.STATE_EXPANDED,
BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT -> {
STATE_EXPANDED,
STATE_HALF_EXPANDED -> {
value = true
}
}
@@ -141,10 +145,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
}
val chargepoints: MediatorLiveData<Resource<List<ChargepointListItem>>> by lazy {
MediatorLiveData<Resource<List<ChargepointListItem>>>()
val chargepoints: MediatorLiveData<Resource<ChargepointList>> by lazy {
MediatorLiveData<Resource<ChargepointList>>()
.apply {
value = Resource.loading(emptyList())
value = Resource.loading(ChargepointList(emptyList(), false))
// this is not automatically updated with mapPosition, as we only want to update
// when map is idle.
listOf(filtersWithValue, repo.api).forEach {
@@ -391,7 +395,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}.distinctUntilChanged()
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null
private var chargepointsInternal: LiveData<Resource<ChargepointList>>? = null
private var chargepointLoader =
throttleLatest(
500L,
@@ -418,13 +422,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
chargepoints.value = Resource.success(chargersClustered)
chargepoints.value = Resource.success(ChargepointList(chargersClustered, true))
return@throttleLatest
}
val result = repo.getChargepoints(bounds, mapPosition.zoom, filters, overrideCache)
chargepointsInternal?.let { chargepoints.removeSource(it) }
chargepointsInternal = result
chargepointsInternal
chargepoints.addSource(result) {
val apiId = apiId.value
when (apiId) {
@@ -471,12 +475,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
private fun extendBounds(bounds: LatLngBounds): LatLngBounds {
val sw = bounds.southwest
val ne = bounds.northeast
var west = sw.longitude - (ne.longitude - sw.longitude) * 0.25
var east = ne.longitude + (ne.longitude + sw.longitude) * 0.25
// do not expand bounds if the map area shown is very large
val expansion = if (ne.longitude - sw.longitude > 10) 1.0 else 1.5
val factor = (expansion - 1.0) * 0.5
var west = sw.longitude - (ne.longitude - sw.longitude) * factor
var east = ne.longitude + (ne.longitude - sw.longitude) * factor
val south =
sw.latitude - (ne.latitude - sw.latitude) * 0.25 * cos(Math.toRadians(sw.latitude))
sw.latitude - (ne.latitude - sw.latitude) * factor * cos(Math.toRadians(sw.latitude))
val north =
ne.latitude + (ne.latitude - sw.latitude) * 0.25 * cos(Math.toRadians(ne.latitude))
ne.latitude + (ne.latitude - sw.latitude) * factor * cos(Math.toRadians(ne.latitude))
if (east - west >= 360) {
west = -180.0

View File

@@ -32,6 +32,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:piv_rtl_mode="auto"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/card"

View File

@@ -19,6 +19,7 @@
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Material3.HeadlineLarge"
android:textAlignment="viewStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"

View File

@@ -6,7 +6,7 @@
<RadioButton
android:id="@+id/rbGoingElectric"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/data_source_goingelectric"
android:textColor="#098ac7"
@@ -21,6 +21,7 @@
android:layout_marginTop="-8dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="32dp"
android:textAlignment="viewStart"
android:text="@string/data_source_goingelectric_desc" />
<RadioButton
@@ -39,6 +40,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="-8dp"
android:layout_marginStart="32dp"
android:textAlignment="viewStart"
android:text="@string/data_source_openchargemap_desc" />
</RadioGroup>

View File

@@ -7,6 +7,8 @@
<import type="java.util.Map" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="java.time.ZonedDateTime" />
<import type="net.vonforst.evmap.model.ChargeLocation" />
@@ -90,495 +92,517 @@
android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp">
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/dragHandle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:paddingTop="8dp"
android:paddingBottom="16dp">
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
android:background="@null"
app:liftOnScroll="true">
<TextView
android:id="@+id/txtName"
android:layout_width="wrap_content"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:hyphenationFrequency="normal"
android:maxLines="@{expanded ? 3 : 1}"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/imgFaultReport"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnless="@{charger.data.address != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtName"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{BindingAdaptersKt.distance(distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
tools:text="10 km" />
<TextView
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepointsMerged, availability.data.status)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView7"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/connectors"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/amenities"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/details" />
<TextView
android:id="@+id/textView11"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.amenities}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView12"
tools:text="Toilet" />
<TextView
android:id="@+id/textView10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/general_info"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView11" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.generalInformation}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textView10"
tools:text="Only for guests" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider3"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<Button
android:id="@+id/sourceButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{@string/source(apiName)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView4"
tools:text="Source: DataSource" />
<TextView
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right|end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == &quot;not signed in&quot; ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
<View
android:id="@+id/topPart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="-10dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/txtName" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
<View
android:id="@+id/divider3"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{predictionData.isPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
<TextView
android:id="@+id/textView29"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@{predictionData.description}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
tools:text="(DC plugs only)" />
<Button
android:id="@+id/btnPredictionHelp"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView8" />
<net.vonforst.evmap.ui.BarGraphView
android:id="@+id/prediction"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
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="@{predictionData.maxValue}"
app:isPercentage="@{predictionData.isPercentage}"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<ImageView
android:id="@+id/imgPredictionSource"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
app:tint="@color/logo_tint_night" />
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/verified"
app:goneUnless="@{ charger.data.verified }"
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_verified"
app:tint="@color/available"
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
tools:targetApi="o" />
<ImageView
android:id="@+id/imgFaultReport"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:contentDescription="@string/fault_report"
app:goneUnless="@{ charger.data.faultReport != null }"
app:layout_constraintEnd_toStartOf="@+id/imgVerified"
app:layout_constraintStart_toEndOf="@+id/txtName"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_map_marker_fault"
app:tooltipTextCompat="@{@string/fault_report}"
tools:targetApi="o" />
<TextView
android:id="@+id/txtTimeRetrieved"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:breakStrategy="balanced"
android:text="@{@string/data_retrieved_at(DateUtils.getRelativeTimeSpanString(charger.data.timeRetrieved.toEpochMilli(), Instant.now().toEpochMilli(), 0))}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.timeRetrieved == null || Duration.between(charger.data.timeRetrieved, Instant.now()).compareTo(Duration.ofHours(1)) > 0}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
tools:text="Data retrieved 4 hours ago" />
<TextView
android:id="@+id/txtLicense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:breakStrategy="balanced"
android:text="@{charger.data.license}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.license != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtTimeRetrieved"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
<HorizontalScrollView
android:id="@+id/buttonsScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constrainedWidth="true"
android:fillViewport="true"
app:goneUnless="@{charger.data != null &amp;&amp; (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
<TextView
android:id="@+id/txtName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice" />
android:ellipsize="end"
android:hyphenationFrequency="normal"
android:maxLines="@{expanded ? 3 : 1}"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
tools:text="Parkhaus" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/verified"
app:goneUnless="@{ charger.data.verified }"
app:srcCompat="@drawable/ic_verified"
app:tint="@color/available"
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
tools:targetApi="o" />
<ImageView
android:id="@+id/imgFaultReport"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/fault_report"
app:goneUnless="@{ charger.data.faultReport != null }"
app:srcCompat="@drawable/ic_map_marker_fault"
app:tooltipTextCompat="@{@string/fault_report}"
tools:targetApi="o" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
android:clipToPadding="false"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:text="@{charger.data.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:invisibleUnless="@{charger.data.address != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:textAlignment="viewEnd"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{BindingAdaptersKt.distance(distance, context)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
tools:text="10 km" />
<TextView
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context), LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:data="@{DataBindingAdaptersKt.chargepointWithAvailability(charger.data.chargepointsMerged, availability.data.status)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView7"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/connectors"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/amenities"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/details" />
<TextView
android:id="@+id/textView11"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.amenities}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView12"
tools:text="Toilet" />
<TextView
android:id="@+id/textView10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/general_info"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView11" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autoLink="web"
android:linksClickable="true"
android:text="@{charger.data.generalInformation}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/textView10"
tools:text="Only for guests" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider3"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<Button
android:id="@+id/btnChargerWebsite"
android:id="@+id/sourceButton"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{@string/source(apiName)}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView4"
tools:text="Source: DataSource" />
<TextView
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == &quot;not signed in&quot; ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
<View
android:id="@+id/topPart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="-10dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
<View
android:id="@+id/divider3"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{predictionData.isPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
<TextView
android:id="@+id/textView29"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/charger_website"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargerUrl != null}"
app:icon="@drawable/ic_link" />
android:layout_marginEnd="8dp"
android:text="@{predictionData.description}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
tools:text="(DC plugs only)" />
</LinearLayout>
</HorizontalScrollView>
<Button
android:id="@+id/btnPredictionHelp"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView8" />
<Button
android:id="@+id/btnLogin"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/login"
app:goneUnless="@{availability.status == Status.ERROR &amp;&amp; availability.message == &quot;not signed in&quot;}"
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/textView13" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:id="@+id/connector_details_card"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@id/connectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp"
android:visibility="gone">
<net.vonforst.evmap.ui.BarGraphView
android:id="@+id/prediction"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginTop="8dp"
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="@{predictionData.maxValue}"
app:isPercentage="@{predictionData.isPercentage}"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
tools:orientation="horizontal" />
<include
layout="@layout/dialog_connector_details"
android:id="@+id/connector_details" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/imgPredictionSource"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionData.predictionGraph != null &amp;&amp; !predictionData.isPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
app:tint="@color/logo_tint_night" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{predictionData.predictionGraph != null}"
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
<TextView
android:id="@+id/txtTimeRetrieved"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:breakStrategy="balanced"
android:text="@{@string/data_retrieved_at(DateUtils.getRelativeTimeSpanString(charger.data.timeRetrieved.toEpochMilli(), Instant.now().toEpochMilli(), 0))}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.timeRetrieved == null || Duration.between(charger.data.timeRetrieved, Instant.now()).compareTo(Duration.ofHours(1)) > 0}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
tools:text="Data retrieved 4 hours ago" />
<TextView
android:id="@+id/txtLicense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:breakStrategy="balanced"
android:text="@{charger.data.license}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textStyle="italic"
app:goneUnless="@{charger.data.license != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/txtTimeRetrieved"
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
<Button
android:id="@+id/btnRefreshLiveData"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/refresh_live_data"
android:enabled="@{availability.status != Status.LOADING}"
app:icon="@drawable/ic_refresh"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
<HorizontalScrollView
android:id="@+id/buttonsScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constrainedWidth="true"
android:fillViewport="true"
app:goneUnless="@{charger.data != null &amp;&amp; (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice" />
<Button
android:id="@+id/btnChargerWebsite"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/charger_website"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargerUrl != null}"
app:icon="@drawable/ic_link" />
</LinearLayout>
</HorizontalScrollView>
<Button
android:id="@+id/btnLogin"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/login"
app:goneUnless="@{availability.status == Status.ERROR &amp;&amp; availability.message == &quot;not signed in&quot;}"
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/textView13" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:id="@+id/connector_details_card"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@id/connectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
android:paddingBottom="@dimen/detail_corner_radius"
app:cardElevation="6dp"
android:visibility="gone">
<include
layout="@layout/dialog_connector_details"
android:id="@+id/connector_details" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@@ -7,6 +7,8 @@
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="net.vonforst.evmap.api.UtilsKt" />
@@ -47,7 +49,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:text="@{String.format(&quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.status == null}"
app:layout_constraintStart_toStartOf="@+id/imageView"
@@ -63,7 +65,7 @@
android:layout_marginTop="30dp"
android:background="@drawable/rounded_rect"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.status}"
@@ -79,7 +81,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="4dp"
android:text="@{item != null ? UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + &quot; · &quot; + item.chargepoint.formatPower() : null}"
android:text="@{item != null ? UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + &quot; · &quot; + item.chargepoint.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context)) : null}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:goneUnless="@{item.chargepoint.hasKnownPower()}"
app:layout_constraintBottom_toTopOf="@id/textView8"

View File

@@ -9,8 +9,6 @@
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike" />
<variable
name="vm"
type="net.vonforst.evmap.viewmodel.MapViewModel" />
@@ -46,7 +44,7 @@
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_behavior="@string/ScrollingAppBarLayoutBehavior">
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
@@ -146,7 +144,7 @@
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="@dimen/gallery_height_with_margin"
android:background="?android:colorBackground"
app:layout_behavior="@string/BackDropBottomSheetBehavior">
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gallery"
@@ -177,35 +175,23 @@
app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior="@string/hide_on_scroll_fab_behavior" />
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
<include
android:id="@+id/detail_view"
layout="@layout/detail_view"
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
app:bottomsheetbehavior_anchorPoint="@dimen/gallery_height"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/peek_height"
app:bottomsheetbehavior_defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
android:clipToPadding="false"
tools:bottomsheetbehavior_defaultState="stateCollapsed">
<include
android:id="@+id/detail_view"
layout="@layout/detail_view"
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionData="@{vm.predictionData}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetExpanded}"
app:apiName="@{vm.apiName}"
app:teslaPricing="@{vm.teslaPricing}" />
</androidx.core.widget.NestedScrollView>
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionData="@{vm.predictionData}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetExpanded}"
app:apiName="@{vm.apiName}"
app:teslaPricing="@{vm.teslaPricing}" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_directions"
@@ -218,16 +204,8 @@
android:translationX="@dimen/directions_fab_translationx"
app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior"
android:theme="@style/NoElevationOverlay" />
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/detail_app_bar"
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.Material3.FloatingActionButton.Small.Surface"
android:id="@+id/fab_layers"

View File

@@ -21,6 +21,7 @@
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:layout_gravity="center_horizontal"
app:piv_rtl_mode="auto"
app:piv_animationType="worm"
app:piv_dynamicCount="true"
app:piv_interactiveAnimation="true"

View File

@@ -96,6 +96,7 @@
android:layout_marginBottom="24dp"
android:paddingStart="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"

View File

@@ -4,6 +4,8 @@
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
@@ -40,7 +42,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:text="@{String.format(&quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView"
@@ -54,7 +56,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:background="@drawable/rounded_rect"
android:padding="2dp"
@@ -72,7 +74,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:text="@{item.chargepoint.formatPower()}"
android:text="@{item.chargepoint.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{item.chargepoint.hasKnownPower()}"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -9,6 +9,8 @@
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<variable
name="item"
type="Chargepoint" />
@@ -51,7 +53,7 @@
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:layout_marginEnd="4dp"
android:text="@{String.format(&quot;× %d&quot;, item.count)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;× %d&quot;, item.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintEnd_toEndOf="parent"
@@ -65,7 +67,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:text="@{item.formatPower()}"
android:text="@{item.formatPower(LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -26,6 +26,7 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="14dp"
android:text="@{item.text}"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -55,6 +56,7 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="14dp"
android:text="@{item.detailText}"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
app:goneUnless="@{item.detailText != null}"

View File

@@ -7,6 +7,8 @@
<import type="net.vonforst.evmap.api.UtilsKt" />
<import type="com.github.erfansn.localeconfigx.LocaleConfigXKt" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
@@ -61,6 +63,7 @@
app:layout_constraintEnd_toStartOf="@+id/textView16"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textAlignment="viewStart"
tools:text="Nikola-Tesla-Parkhaus mit extra langem Namen, der auf mehrere Zeilen umbricht" />
<TextView
@@ -72,6 +75,7 @@
android:maxLines="1"
android:text="@{item.charger.address.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textAlignment="viewStart"
app:invisibleUnless="@{item.charger.address != null}"
app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="parent"
@@ -85,8 +89,9 @@
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context), LocaleConfigXKt.getCurrentOrDefaultLocale(context))}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2"
@@ -111,7 +116,7 @@
android:layout_marginEnd="16dp"
android:background="@drawable/rounded_rect"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
android:text="@{String.format(LocaleConfigXKt.getCurrentOrDefaultLocale(context), &quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{item.available.data}"

View File

@@ -29,6 +29,7 @@
android:layout_marginBottom="16dp"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch1"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -32,6 +32,7 @@
android:layout_marginTop="16dp"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Connectors" />

View File

@@ -33,6 +33,7 @@
android:layout_marginEnd="8dp"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -61,6 +62,7 @@
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView17"

View File

@@ -30,6 +30,7 @@
android:layout_marginEnd="16dp"
android:text="@string/map_type"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/btnClose"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -52,7 +53,8 @@
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.NORMAL)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.NORMAL)}"
android:text="@string/map_type_normal" />
android:text="@string/map_type_normal"
android:textAlignment="viewStart" />
<RadioButton
android:id="@+id/rbSatellite"
@@ -60,7 +62,8 @@
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.HYBRID)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.HYBRID)}"
android:text="@string/map_type_satellite" />
android:text="@string/map_type_satellite"
android:textAlignment="viewStart" />
<RadioButton
android:id="@+id/rbTerrain"
@@ -68,7 +71,8 @@
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(AnyMap.Type.TERRAIN)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.TERRAIN)}"
android:text="@string/map_type_terrain" />
android:text="@string/map_type_terrain"
android:textAlignment="viewStart" />
</RadioGroup>
<TextView
@@ -80,6 +84,7 @@
android:layout_marginEnd="16dp"
android:text="@string/map_details"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textAlignment="viewStart"
app:goneUnless="@{vm.mapTrafficSupported}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
@@ -95,6 +100,7 @@
android:layout_marginEnd="16dp"
android:text="@string/map_traffic"
android:checked="@={vm.mapTrafficEnabled}"
android:textAlignment="viewStart"
app:goneUnless="@{vm.mapTrafficSupported}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -101,6 +101,7 @@
<string name="pref_language">App-Sprache</string>
<string name="pref_darkmode">Dunkles Design</string>
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
<string name="zoom_in_to_see_more">Hineinzoomen um alle Ladestationen zu sehen</string>
<string name="location_error">Standort nicht erkannt. Bitte Systemeinstellungen prüfen</string>
<string name="retry">Wiederholen</string>
<string name="filter_open_247">24 Stunden geöffnet</string>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="directions_fab_translationx">44dp</dimen>
</resources>

View File

@@ -12,4 +12,5 @@
<dimen name="map_toolbar_width">@dimen/match_parent</dimen>
<dimen name="layers_fab_top_padding">100dp</dimen>
<dimen name="directions_fab_translationx">0dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@@ -101,6 +101,7 @@
<string name="pref_language">App language</string>
<string name="pref_darkmode">Dark mode</string>
<string name="connection_error">Could not load charging stations</string>
<string name="zoom_in_to_see_more">Zoom in to see all charging stations</string>
<string name="location_error">Failed to detect location. Please check system settings</string>
<string name="retry">Retry</string>
<string name="filter_open_247">Available 24/7</string>

View File

@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion by extra("1.9.24")
val kotlinVersion by extra("2.0.21")
val aboutLibsVersion by extra("10.9.1")
val navVersion by extra("2.7.7")
repositories {
@@ -10,7 +10,7 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:8.4.2")
classpath("com.android.tools.build:gradle:8.9.3")
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")

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Anzeigefehler behoben

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Anzeigefehler behoben
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Echtzeitdaten für Tesla Supercharger repariert

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Abstürze behoben
- Aktueller Standort wurde nicht immer angezeigt, obwohl verfügbar

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed display errors

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed display errors
- Fixed crashes

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed realtime data for Tesla Superchargers

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed crashes
- Fixed current location not being shown despite it being available

View File

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