Compare commits

..

71 Commits

Author SHA1 Message Date
johan12345
4f6f09dc83 Release 1.1.2 2021-11-14 18:17:39 +01:00
johan12345
7f6d0c1391 update dependencies 2021-11-14 17:59:41 +01:00
johan12345
96b60d0f49 GoingElectric API: do not show "paid parking" / "paid charging"
because that may also mean that no information is available
see #13
2021-11-14 17:33:36 +01:00
johan12345
2824f0b5c3 handle saved state for MapViewModel 2021-11-14 17:19:11 +01:00
johan12345
af0921ed20 implement a93bacd9b3 for Android Auto 2021-11-14 16:58:44 +01:00
johan12345
a5b55479cb Detail view: show opening hours description also if open 24/7 2021-11-14 16:40:06 +01:00
johan12345
a93bacd9b3 Chargeprice: show provider-exclusive plans when included in "my plans"
fixes #147
2021-11-14 16:26:43 +01:00
johan12345
9d7278e0e2 AvailabilityDetector: support missing household plugs in NewMotion data
fixes #146
2021-11-14 15:58:08 +01:00
johan12345
f6d9c615a0 AvailabilityDetectorTest: use proper socket type constants 2021-11-14 15:40:13 +01:00
johan12345
a8ee3f5b7d Change semantics of opening hours in model
to fix incompatibility with Room that caused a NullPointerException
2021-11-14 15:27:49 +01:00
johan12345
826b4f89f1 fix crash in light mode
introduced in 5d7d881729
2021-11-06 22:51:16 +01:00
johan12345
5675d065e3 update Car App Library to 1.1.0-rc01 2021-11-06 22:34:42 +01:00
johan12345
3e3531551d add link to Chargeprice FAQ page 2021-11-06 22:30:31 +01:00
johan12345
5d7d881729 Chargeprice: add branding 2021-11-06 21:29:17 +01:00
johan12345
23c73e3d7e Android Auto: add error message when Chargeprice data fails to load 2021-11-06 20:02:02 +01:00
johan12345
7835aa8d78 initialize Google map with correct locale 2021-11-04 21:47:33 +01:00
johan12345
f06b712090 disable NullSafeMutableLiveData lint error
seems to give false positives
2021-11-01 19:22:34 +01:00
johan12345
317695954d remove unneeded maven repositories 2021-11-01 15:27:56 +01:00
johan12345
24cfd1c10b upgrade dependencies 2021-11-01 15:16:21 +01:00
johan12345
775faa2f55 update AboutLibraries 2021-11-01 15:00:45 +01:00
johan12345
08bd2bdf5a update build tools 2021-11-01 15:00:30 +01:00
johan12345
90254915e3 Release 1.1.1 2021-11-01 13:10:37 +01:00
johan12345
b7f56ecff4 fix detection of imperial units when locale is "unspecified English" 2021-11-01 13:03:50 +01:00
johan12345
fa3910d3c8 avoid NPE when country == null 2021-11-01 12:47:05 +01:00
johan12345
4500c55560 Android Auto: throttle updates of distances
to mitigate bug in AA https://issuetracker.google.com/issues/204692002
2021-11-01 12:39:59 +01:00
johan12345
a493e1a548 Android Auto MapScreen: show distances in correct units 2021-11-01 12:03:27 +01:00
johan12345
ddaab42e45 update filteredConnectors before chargepoints
fixes incorrect marker colors
2021-11-01 10:37:46 +01:00
johan12345
9f50341ab7 use latest Google Maps renderer 2021-11-01 10:26:30 +01:00
johan12345
9966b44a76 Release 1.1.0 2021-10-17 21:29:53 +02:00
johan12345
d44b2206d2 update build status badge 2021-10-17 15:49:56 +02:00
johan12345
f61082f491 fix incorrect formatting in api_keys.md 2021-10-17 15:27:01 +02:00
johan12345
f58d96c939 fix links in README 2021-10-17 15:26:08 +02:00
Johan von Forstner
29aedfa3d9 Merge pull request #140 from johan12345/contributing-docs
Improve docs for first-time contributors
2021-10-17 15:25:15 +02:00
johan12345
8331f92f10 Improve docs for first-time contributors
adds two docs pages with info on API keys and Android Auto testing.
fixes #112
2021-10-17 15:24:50 +02:00
johan12345
123680d3e8 replace Mapbox android-plugin-places with just the Java SDK
(the UI has been replaced by our own one in #120)
2021-10-17 12:43:16 +02:00
johan12345
0f6b45d745 upgrade navigation component to 2.4.0-alpha10
(necessary for Android 12 compatibility of deep links)
2021-10-17 12:11:28 +02:00
Johan von Forstner
69faa94f18 Merge pull request #139 from pt2121/pt/safeArgs
refactor to use Android Navigation Component's SafeArgs
2021-10-17 12:10:16 +02:00
prat t
70805b7960 refactor to use Navigation Component's SafeArgs 2021-10-16 15:16:56 -07:00
johan12345
56453b0658 remove obsolete TODO 2021-10-16 15:51:36 +02:00
johan12345
975d95e37e refactor app settings into separate sub-screens
for better overview
2021-10-16 15:49:00 +02:00
johan12345
ba34cd016a fix static splashscreen icon size
after upgrade to core-splashscreen 1.0.0-alpha02 in e2bcf8d1
2021-10-16 12:36:29 +02:00
johan12345
590b16aa49 make debug build distinguishable from release build
with grayscale app icon + different label
fixes #113
2021-10-16 12:25:26 +02:00
johan12345
5fe8d0cab4 replace Google Maps v3 beta with Play Services version 17.0.1
fixes #124
fixes #30
2021-10-16 12:16:27 +02:00
johan12345
9d7b181410 remove focus from search when selecting a charger 2021-10-10 17:55:16 +02:00
johan12345
128532aac6 open autocomplete list more quickly 2021-10-10 17:51:54 +02:00
johan12345
486854f56c Android Auto: use slightly darker color for >100 kW chargers for better contrast 2021-10-09 19:16:21 +02:00
johan12345
1e30db5cd1 GoingElectricApi: map plug types correctly to names 2021-10-09 13:55:25 +02:00
johan12345
aad386ab04 MultiSelectDialog: put common choices on top even when selected 2021-10-09 13:44:17 +02:00
johan12345
e2bcf8d1cd upgrade dependencies 2021-10-08 22:02:04 +02:00
Johan von Forstner
f56fad1282 Merge pull request #134 from johan12345/filter-by-favorites
add possibility to show only the favorites on a map
2021-10-08 21:51:46 +02:00
johan12345
adb4d938cc add possibility to show only the favorites on a map (fixes #119) 2021-10-08 21:51:13 +02:00
Johan von Forstner
b773f65912 Update screenshot URLs 2021-10-06 08:39:13 +02:00
johan12345
de335b18d8 PlaceAutocompleteAdapter: do not modify resultList on background thread 2021-10-05 21:43:47 +02:00
johan12345
6c8380b8ce revisit Android Auto location service connection
to possibly fix IllegalArgumentException: Service not registered
based on https://stackoverflow.com/questions/22079909/android-java-lang-illegalargumentexception-service-not-registered
2021-10-05 21:12:03 +02:00
Johan von Forstner
81afdca19d Merge pull request #130 from johan12345/highlight_favorites
highlight favorites on map
2021-10-03 13:33:12 +02:00
johan12345
14e03ba6dd highlight favorites on map
fixes #118
2021-10-03 13:31:58 +02:00
Johan von Forstner
abe12b45c3 disable git LFS for screenshots
(not supported on F-Droid
2021-10-03 12:55:11 +02:00
johan12345
23387ae371 Release 1.0.0 2021-10-02 14:08:44 +02:00
johan12345
25f466b6d7 Merge branch 'place-search-recents' 2021-10-02 13:49:25 +02:00
johan12345
6692b21bf9 add non-dark Mapbox logo 2021-10-02 13:48:50 +02:00
johan12345
5959fe8be4 limit number of autocomplete results 2021-10-02 13:40:22 +02:00
johan12345
00f4c13fcc add button to delete search history 2021-10-02 13:33:51 +02:00
johan12345
47054d470b replace deprecated method getAdapterPosition 2021-10-02 13:23:30 +02:00
Johan von Forstner
d10192cae1 save recent place search results
fixes #128
2021-10-02 13:19:33 +02:00
johan12345
e1b90955c3 handle back button correctly
fixes #115
2021-10-02 13:18:27 +02:00
johan12345
d249bf47c7 improve title of filter profiles editing 2021-10-02 13:15:58 +02:00
johan12345
738dcd5f8d do not allow blank names for filter profiles 2021-10-02 13:11:05 +02:00
johan12345
ad4f32ec32 fix missing filter name in title of FilterFragment 2021-10-02 12:47:22 +02:00
Johan von Forstner
4d03107ae7 add links to translated FAQ and privacy policy
#110
2021-09-26 16:17:44 +02:00
Johan von Forstner
0e93e310bf MultiSelectDialog: move selected entries to the top
fixes #126
2021-09-26 16:17:44 +02:00
Johan von Forstner
6cb8940696 Catch IllegalArgumentException in navigation library 2021-09-26 14:47:55 +02:00
131 changed files with 1917 additions and 623 deletions

View File

@@ -1,4 +1,4 @@
EVMap [![Build Status](https://travis-ci.org/johan12345/EVMap.svg?branch=master)](https://travis-ci.org/johan12345/EVMap)
EVMap [![Build Status](https://app.travis-ci.com/johan12345/EVMap.svg?branch=master)](https://app.travis-ci.com/johan12345/EVMap)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
@@ -28,38 +28,22 @@ Features
Screenshots
-----------
<img src="https://media.githubusercontent.com/media/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://media.githubusercontent.com/media/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
Development setup
-----------------
The App is developed using Android Studio.
The App is developed using Android Studio and should pretty much work out-of-the-box when you clone
the Git repository and open the project with Android Studio.
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs),
the [OpenChargeMap API](https://openchargemap.org/site/profile/appedit),
as well as for [Google APIs](https://console.developers.google.com/)
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These API keys need to be put into the
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
following content:
The only exception is that you need to obtain some free API keys for the different data sources that
EVMap uses and put them into the app in the form of a resource file called `apikeys.xml` under
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md).
```xml
<resources>
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
insert your Google Maps key here
</string>
<string name="mapbox_key" translatable="false">
insert your Mapbox key here
</string>
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
</resources>
```
There are two different build flavors, `google` and `foss`, where only the `google` variant uses
Google Maps data and provides the Android Auto integration. The `foss` variant only uses Mapbox data
and should run on devices without Google Play Services.
We also have a special [documentation page](doc/android_auto.md) on how to test the Android Auto
app.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 194 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 242 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 90 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 352 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 113 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 875 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 341 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 134 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 343 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 83 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 106 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 864 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 330 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 95 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 31
versionCode 62
versionName "1.0.0-beta04"
versionCode 66
versionName "1.1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -103,22 +103,25 @@ android {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
}
lintOptions {
disable 'NullSafeMutableLiveData'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
implementation "androidx.activity:activity-ktx:1.3.1"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.browser:browser:1.4.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
@@ -140,43 +143,29 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
googleImplementation 'androidx.car.app:app:1.1.0-beta01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
googleImplementation 'androidx.car.app:app:1.1.0-rc01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-rc01'
// AnyMaps
def anyMapsVersion = '95ddd6c083'
def anyMapsVersion = '751daec281'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.0.0'
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
// Google Maps v3 Beta
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
googleImplementation 'com.android.volley:volley:1.2.0'
googleImplementation 'com.google.android.gms:play-services-base:17.5.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.5.0'
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
googleImplementation 'com.google.android.gms:play-services-location:17.1.0'
googleImplementation 'com.google.android.gms:play-services-tasks:17.2.0'
googleImplementation 'com.google.auto.value:auto-value-annotations:1.6.3'
googleImplementation 'com.google.code.gson:gson:2.8.6'
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.5'
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Google Places
implementation 'com.google.android.libraries.places:places:2.5.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
// Mapbox places (autocomplete)
// forked this library and included through JitPack to fix https://github.com/mapbox/mapbox-plugins-android/issues/1011
implementation('com.github.johan12345.mapbox-plugins-android:mapbox-android-plugin-places-v9:922bf877f6') {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
// navigation library
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.3.1"
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
@@ -200,7 +189,7 @@ dependencies {
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'

View File

Binary file not shown.

View File

@@ -0,0 +1,107 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="198.3471"
android:viewportHeight="198.3471">
<group
android:translateX="3.1735537"
android:translateY="3.1735537">
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M106.2,74.3h-7"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M106.2,60.3c0,0 -17.5,0 -17.5,0"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M93.9,79.5L88.7,79.5"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M94,79v16.2"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M106.2,60.3L99.2,74.3"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M106.2,74.3L93.9,95.2"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<path
android:pathData="M67.6,120.6L65.7,104l-2.9,0.3l1.9,16.6L67.6,120.6zM77.9,119.4l-1.9,-16.6l-2.9,0.3l1.9,16.6L77.9,119.4z"
android:fillColor="#808080" />
<path
android:pathData="M83.3,142c-0.9,1.1 -1.6,1.8 -1.7,1.9c-2.6,2.1 -4.7,2.7 -6.4,1.9c-3,-1.5 -2.8,-7.1 -2.7,-7.7l2.1,0.1c-0.1,1.6 0.2,5 1.6,5.7c0.8,0.4 2.2,-0.1 4,-1.6l0,0c0,0 5.8,-5.8 4.6,-10.4c-1.4,-5.5 5,-13.4 7.1,-16.1l0.3,-0.3l1.7,1.3l-0.3,0.4c-6.5,8 -7.2,12.1 -6.7,14.2C87.9,135.4 85.2,139.7 83.3,142z"
android:fillColor="#9e9e9e" />
<path
android:pathData="M61.2,120.4l0.8,6.8l6.3,4.2l8.5,-0.9l5.2,-5.5l-0.8,-6.8L61.2,120.4z"
android:fillColor="#9e9e9e" />
<path
android:pathData="M76.7,130.5l-8.5,0.9l1.8,7.5l6.7,-0.8L76.7,130.5L76.7,130.5zM82.8,112.5l0.7,6.2l-24.4,2.8l-0.7,-6.2L82.8,112.5z"
android:fillColor="#666666" />
<path
android:pathData="M101.9,44.1c-17.5,0 -31.7,14.2 -31.7,31.7c0,23.9 26.7,36.4 29.9,70.5c0.1,1 0.9,1.7 1.9,1.7s1.8,-0.7 1.9,-1.7c3.2,-34.1 29.9,-46.6 29.9,-70.5C133.6,58.2 119.4,44.1 101.9,44.1z"
android:fillColor="#737373" />
<path
android:pathData="M101.9,44.8c17.4,0 31.5,14 31.7,31.3c0,-0.1 0,-0.2 0,-0.3c0,-17.5 -14.2,-31.7 -31.7,-31.7S70.2,58.2 70.2,75.8c0,0.1 0,0.2 0,0.3C70.4,58.8 84.5,44.8 101.9,44.8L101.9,44.8z"
android:fillColor="#FFFFFF"
android:fillAlpha="0.2" />
<path
android:pathData="M103.8,145.5c-0.1,1 -0.9,1.7 -1.9,1.7s-1.8,-0.7 -1.9,-1.7c-3.1,-34 -29.6,-46.5 -29.8,-70.1c0,0.2 0,0.3 0,0.5c0,23.9 26.7,36.4 29.9,70.5c0.1,1 0.9,1.7 1.9,1.7s1.8,-0.7 1.9,-1.7c3.2,-34.1 29.9,-46.6 29.9,-70.5c0,-0.2 0,-0.3 0,-0.5C133.4,99 106.9,111.5 103.8,145.5L103.8,145.5z"
android:fillColor="#303030"
android:fillAlpha="0.2" />
<path
android:fillColor="#FF000000"
android:pathData="M94.6,60.3v19.2h5.2v15.7l12.2,-21h-7l7,-14C112.1,60.3 94.6,60.3 94.6,60.3z"
android:strokeAlpha="0.45"
android:fillAlpha="0.45" />
</group>
</vector>

View File

@@ -5,10 +5,18 @@ import android.content.Context
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.maps.MapsInitializer
import com.google.android.libraries.places.api.Places
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
fun init(context: Context) {
Places.initialize(context, context.getString(R.string.google_maps_key));
Places.initialize(context, context.getString(R.string.google_maps_key))
val localeContext = LocaleContextWrapper.wrap(
context.applicationContext, PreferenceDataSource(context).language
)
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
}
fun checkPlayServices(activity: Activity): Boolean {

View File

@@ -62,6 +62,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
locationService = null
}
}
private var serviceBound = false
init {
lifecycle.addObserver(this)
@@ -91,13 +92,9 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
private fun onCarHardwareLocationReceived(loc: CarHardwareLocation) {
updateLocation(loc.location.value)
locationService?.let { service ->
// we successfully received a location from the car hardware,
// so we don't need the smartphone location anymore.
service.removeLocationUpdates()
cas.unbindService(serviceConnection)
locationService = null
}
// we successfully received a location from the car hardware,
// so we don't need the smartphone location anymore.
unbindLocationService()
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
@@ -111,7 +108,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
::onCarHardwareLocationReceived
)
}
cas.bindService(
serviceBound = cas.bindService(
Intent(cas, CarLocationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
@@ -119,13 +116,18 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun unbindLocationService() {
private fun onStop() {
if (supportsCarApiLevel3(carContext)) {
hardwareMan.carSensors.removeCarHardwareLocationListener(::onCarHardwareLocationReceived)
}
locationService?.let { service ->
service.removeLocationUpdates()
unbindLocationService()
}
private fun unbindLocationService() {
locationService?.removeLocationUpdates()
if (serviceBound) {
cas.unbindService(serviceConnection)
serviceBound = false
}
}

View File

@@ -14,14 +14,20 @@ import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.*
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
@@ -164,94 +170,120 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val manufacturer = model?.manufacturer?.value
val modelName = model?.name?.value
lifecycleScope.launch {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
try {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
invalidate()
return@launch
}
} else {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
invalidate()
return@launch
}
} else {
}
val car = vehicles[0]
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
tariffs = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
}
val car = vehicles[0]
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage = carContext.getString(R.string.chargeprice_no_compatible_connectors)
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
return@launch
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.chargeprice_connection_error,
CarToast.LENGTH_LONG
)
.show()
}
}
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
}
}

View File

@@ -7,7 +7,9 @@ import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import net.vonforst.evmap.R
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.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -40,9 +42,12 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
}
override fun onGetTemplate(): Template {
val filterStatus =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
return ListTemplate.Builder().apply {
filterProfiles.value?.let {
setSingleList(buildFilterProfilesList(it.take(maxRows), prefs.filterStatus))
setSingleList(buildFilterProfilesList(it.take(maxRows), filterStatus))
} ?: setLoading(true)
setTitle(carContext.getString(R.string.menu_filter))
setHeaderAction(Action.BACK)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.text.SpannableStringBuilder
import android.text.Spanned
@@ -7,10 +8,14 @@ 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.hardware.CarHardwareManager
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
@@ -22,6 +27,7 @@ import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
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.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
@@ -33,6 +39,7 @@ import net.vonforst.evmap.viewmodel.getFilters
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.roundToInt
@@ -51,7 +58,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val maxNumUpdates = 1
private var location: Location? = null
private var lastUpdateLocation: Location? = null
private var lastChargerUpdateLocation: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
@@ -59,7 +67,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
createApi(prefs.dataSource, ctx)
}
private val searchRadius = 5 // kilometers
private val updateThreshold = 2000 // meters
private val chargerUpdateThreshold = 2000 // meters
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
HashMap()
@@ -69,12 +78,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM } ?: FILTERS_DISABLED
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters = api.getFilters(referenceData, carContext.stringProvider())
private val filtersWithValue = filtersWithValue(filters, filterValues)
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
private var energyLevel: EnergyLevel? = null
init {
filtersWithValue.observe(this) {
loadChargers()
@@ -139,7 +152,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
screenManager.pushForResult(FilterScreen(carContext)) {
chargers = null
numUpdates = 0
filterStatus.value = prefs.filterStatus
filterStatus.value =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
}
session.mapScreen = null
}
@@ -151,7 +166,12 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
val markerTint = if (charger.maxPower > 100) {
R.color.charger_100kw_dark // slightly darker color for better contrast
} else {
getMarkerTint(charger)
}
val color = ContextCompat.getColor(carContext, markerTint)
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
.setMarker(
@@ -177,13 +197,18 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
// distance
location?.let {
val distance = distanceBetween(
val distanceMeters = distanceBetween(
it.latitude, it.longitude,
charger.coordinates.lat, charger.coordinates.lng
) / 1000
)
text.append(
"distance",
DistanceSpan.create(Distance.create(distance, Distance.UNIT_KILOMETERS)),
DistanceSpan.create(
roundValueToDistance(
distanceMeters,
energyLevel?.distanceDisplayUnit?.value
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
@@ -231,12 +256,19 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
return
}
invalidate()
if (lastUpdateLocation == null ||
location.distanceTo(lastUpdateLocation) > updateThreshold
val now = Instant.now()
if (lastDistanceUpdateTime == null ||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
) {
lastUpdateLocation = location
lastDistanceUpdateTime = now
// update displayed distances
invalidate()
}
if (lastChargerUpdateLocation == null ||
location.distanceTo(lastChargerUpdateLocation) > chargerUpdateThreshold
) {
lastChargerUpdateLocation = location
// update displayed chargers
loadChargers()
}
@@ -312,6 +344,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}?.awaitAll()
updateCoroutine = null
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
@@ -321,4 +354,30 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
}
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
this.energyLevel = energyLevel
invalidate()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun setupListeners() {
if (ContextCompat.checkSelfPermission(
carContext,
"com.google.android.gms.permission.CAR_FUEL"
) != PackageManager.PERMISSION_GRANTED
)
return
println("Setting up energy level listener")
val exec = ContextCompat.getMainExecutor(carContext)
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun removeListeners() {
println("Removing energy level listener")
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
}
}

View File

@@ -7,10 +7,12 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
import kotlin.math.roundToInt
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
@@ -34,14 +36,22 @@ val CarContext.constraintManager
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
private const val kmPerMile = 1.609344
private const val ftPerMile = 5280
private const val ydPerMile = 1760
fun getDefaultDistanceUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILE
else -> CarUnit.KILOMETER
return if (usesImperialUnits(Locale.getDefault())) {
CarUnit.MILE
} else {
CarUnit.KILOMETER
}
}
fun usesImperialUnits(locale: Locale): Boolean {
return locale.country in listOf("US", "GB", "MM", "LR")
|| locale.country == "" && locale.language == "en"
}
fun getDefaultSpeedUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
@@ -72,6 +82,52 @@ fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
}
}
fun roundValueToDistance(value: Double, unit: Int? = null): Distance {
// value is in meters
when (unit ?: getDefaultDistanceUnit()) {
CarUnit.MILE -> {
// imperial system
val miles = value / 1000 / kmPerMile
val yards = miles * ydPerMile
val feet = miles * ftPerMile
return when (miles) {
in 0.0..0.1 -> if (Locale.getDefault().country == "UK") {
Distance.create(roundToMultipleOf(yards, 10.0), Distance.UNIT_YARDS)
} else {
Distance.create(roundToMultipleOf(feet, 10.0), Distance.UNIT_FEET)
}
in 0.1..10.0 -> Distance.create(
roundToMultipleOf(miles, 0.1),
Distance.UNIT_MILES_P1
)
else -> Distance.create(roundToMultipleOf(miles, 1.0), Distance.UNIT_MILES)
}
}
else -> {
// metric system
return when (value) {
in 0.0..999.0 -> Distance.create(
roundToMultipleOf(value, 10.0),
Distance.UNIT_METERS
)
in 1000.0..10000.0 -> Distance.create(
roundToMultipleOf(value / 1000, 0.1),
Distance.UNIT_KILOMETERS_P1
)
else -> Distance.create(
roundToMultipleOf(value / 1000, 1.0),
Distance.UNIT_KILOMETERS
)
}
}
}
}
private fun roundToMultipleOf(num: Double, step: Double): Double {
return (num / step).roundToInt() * step
}
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")

View File

@@ -7,9 +7,9 @@ import android.text.style.StyleSpan
import com.car2go.maps.google.adapter.AnyMapAdapter
import com.car2go.maps.util.SphericalUtil
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.tasks.Tasks.await
import com.google.android.libraries.maps.model.LatLng
import com.google.android.libraries.maps.model.LatLngBounds
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.model.Place
@@ -28,6 +28,8 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
private val client = Places.createClient(context)
private val bold: CharacterStyle = StyleSpan(Typeface.BOLD)
override val id = "google"
override fun autocomplete(
query: String,
location: com.car2go.maps.model.LatLng?
@@ -48,7 +50,7 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
it.getPrimaryText(bold),
it.getSecondaryText(bold),
it.placeId,
it.distanceMeters,
it.distanceMeters?.toDouble(),
it.placeTypes.map { AutocompletePlaceType.valueOf(it.name) })
}
} catch (e: ExecutionException) {

View File

@@ -3,4 +3,5 @@
<color name="gauge_active">#00e676</color>
<color name="gauge_middle">#087f23</color>
<color name="gauge_inactive">#9e9e9e</color>
<color name="charger_100kw_dark">#fdd835</color>
</resources>

View File

@@ -32,7 +32,7 @@
<activity
android:name=".MapsActivity"
android:label="@string/title_activity_maps"
android:label="@string/app_name"
android:theme="@style/AppTheme.LaunchScreen"
android:exported="true">
<intent-filter>

View File

@@ -19,11 +19,15 @@ import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.car2go.maps.model.LatLng
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.fragment.MapFragment
import net.vonforst.evmap.fragment.MapFragmentArgs
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
@@ -35,7 +39,8 @@ const val EXTRA_CHARGER_ID = "chargerId"
const val EXTRA_LAT = "lat"
const val EXTRA_LON = "lon"
class MapsActivity : AppCompatActivity() {
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
@@ -102,11 +107,11 @@ class MapsActivity : AppCompatActivity() {
}
})
}
navGraph.startDestination = R.id.onboarding
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else {
navGraph.startDestination = R.id.map
navGraph.setStartDestination(R.id.map)
navController.graph = navGraph
}
@@ -121,14 +126,14 @@ class MapsActivity : AppCompatActivity() {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocation(lat, lon))
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
deepLink.send()
} else if (query != null && query.isNotEmpty()) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocationByName(query))
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
deepLink.send()
}
@@ -138,7 +143,7 @@ class MapsActivity : AppCompatActivity() {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showChargerById(id))
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
deepLink.send()
}
@@ -146,11 +151,13 @@ class MapsActivity : AppCompatActivity() {
navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragment.showCharger(
intent.getLongExtra(EXTRA_CHARGER_ID, 0),
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
.send()
@@ -213,4 +220,15 @@ class MapsActivity : AppCompatActivity() {
}
startActivity(intent)
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Identify the Navigation Destination
val navDestination = navController.graph
.find { target -> target is FragmentNavigator.Destination && pref.fragment == target.className }
navDestination?.let { target -> navController.navigate(target.id) }
return true
}
}

View File

@@ -84,10 +84,10 @@ fun buildDetails(
loc.openinghours.getStatusText(ctx)
else
loc.openinghours.description ?: "",
if (loc.openinghours.days != null) loc.openinghours.description else null,
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven) loc.openinghours.description else null,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailsAdapter.Detail(
if (loc.cost != null && !loc.cost.isEmpty) DetailsAdapter.Detail(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),

View File

@@ -1,6 +1,8 @@
package net.vonforst.evmap.adapter
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -13,7 +15,10 @@ import net.vonforst.evmap.autocomplete.*
import net.vonforst.evmap.containsAny
import net.vonforst.evmap.databinding.ItemAutocompleteResultBinding
import net.vonforst.evmap.isDarkMode
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.RecentAutocompletePlace
import java.time.Instant
class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatLng>) :
BaseAdapter(), Filterable {
@@ -21,7 +26,10 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
private val providers = getAutocompleteProviders(context)
private val typeItem = 0
private val typeAttribution = 1
var currentProvider: AutocompleteProvider? = null
private val maxItems = 6
private var currentProvider: AutocompleteProvider? = null
private val recents = AppDatabase.getInstance(context).recentAutocompletePlaceDao()
private var recentResults = mutableListOf<RecentAutocompletePlace>()
data class ViewHolder(val binding: ItemAutocompleteResultBinding)
@@ -90,11 +98,12 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
init {
if (PreferenceDataSource(context).searchProvider == "mapbox") {
// set delay to 500 ms to reduce paid Mapbox API requests
this.setDelayer { 500L }
this.setDelayer { q -> if (isShortQuery(q)) 0L else 500L }
}
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
resultList = results?.values as? List<AutocompletePlace>?
if (results != null && results.count > 0) {
notifyDataSetChanged()
} else {
@@ -103,38 +112,86 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
}
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterResults = FilterResults()
val query = constraint.toString()
var resultList: List<AutocompletePlace>? = null
if (constraint != null) {
for (provider in providers) {
try {
resultList =
provider.autocomplete(constraint.toString(), location.value)
recentResults.clear()
currentProvider = provider
// first search in recent places
val recentPlaces = if (query.isEmpty()) {
recents.getAll(provider.id, limit = maxItems)
} else {
recents.search(query, provider.id, limit = maxItems)
}
recentResults.addAll(recentPlaces)
resultList = recentPlaces.map { it.asAutocompletePlace(location.value) }
Handler(Looper.getMainLooper()).post {
// publish intermediate results on main thread
publishResults(constraint, resultList.asFilterResults())
}
// if we already have enough results or the query is short, stop here
if (isShortQuery(query) || recentResults.size >= maxItems) break
// then search online
val recentIds = recentPlaces.map { it.id }
resultList =
(resultList!! + provider.autocomplete(query, location.value)
.filter { !recentIds.contains(it.id) }).take(maxItems)
break
} catch (e: ApiUnavailableException) {
e.printStackTrace()
}
}
filterResults.values = resultList
filterResults.count = resultList!!.size
}
if (currentProvider is MapboxAutocompleteProvider && !delaySet) {
// set delay to 500 ms to reduce paid Mapbox API requests
this.setDelayer { 500L }
this.setDelayer { q -> if (isShortQuery(q)) 0L else 500L }
}
return filterResults
return resultList.asFilterResults()
}
private fun List<AutocompletePlace>?.asFilterResults(): FilterResults {
val result = FilterResults()
if (this != null) {
result.values = this
result.count = this.size
}
return result
}
}
}
private fun isShortQuery(query: CharSequence) = query.length < 3
suspend fun getDetails(id: String): PlaceWithBounds {
val provider = currentProvider!!
val result = resultList!!.find { it.id == id }!!
val recentPlace = recentResults.find { it.id == id }
if (recentPlace != null) return recentPlace.asPlaceWithBounds()
val details = provider.getDetails(id)
recents.insert(RecentAutocompletePlace(result, details, provider.id, Instant.now()))
return details
}
}
fun iconForPlaceType(types: List<AutocompletePlaceType>): Int =
when {
types.contains(
AutocompletePlaceType.RECENT
) -> R.drawable.ic_history
types.containsAny(
AutocompletePlaceType.LIGHT_RAIL_STATION,
AutocompletePlaceType.BUS_STATION,
@@ -153,4 +210,7 @@ fun iconForPlaceType(types: List<AutocompletePlaceType>): Int =
}
fun isSpecialPlace(types: List<AutocompletePlaceType>): Boolean =
iconForPlaceType(types) != R.drawable.ic_place_type_default
!setOf(
R.drawable.ic_place_type_default,
R.drawable.ic_history
).contains(iconForPlaceType(types))

View File

@@ -65,10 +65,20 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
var chargepoints = chargepoints
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
val geTypes = chargepoints.map { it.type }.distinct().toSet()
var geTypes = chargepoints.map { it.type }.distinct().toSet()
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
Chargepoint.SCHUKO
)) {
// If charger has household plugs and other plugs, try removing the household plugs
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
}
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
// find connectors of this type
@@ -92,7 +102,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
chargepoint to ids
}
} else if (powers.size == 1 && gePowers.size == 2
&& chargepoints.sumBy { it.count } == connsOfType.size
&& chargepoints.sumOf { it.count } == connsOfType.size
) {
// special case: dual charger(s) with load balancing
// GoingElectric shows 2 different powers, NewMotion just one

View File

@@ -205,6 +205,9 @@ class ChargePrice : Resource(), Equatable, Cloneable {
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
@field:Json(name = "branding")
var branding: ChargepriceBranding? = null
var tariff: HasOne<ChargepriceTariff>? = null
@@ -238,6 +241,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
if (startTime != other.startTime) return false
if (tags != other.tags) return false
if (chargepointPrices != other.chargepointPrices) return false
if (branding != other.branding) return false
return true
}
@@ -256,6 +260,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
result = 31 * result + startTime
result = 31 * result + tags.hashCode()
result = 31 * result + chargepointPrices.hashCode()
result = 31 * result + branding.hashCode()
return result
}
@@ -274,6 +279,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
branding = this@ChargePrice.branding
}
}
}
@@ -328,6 +334,12 @@ data class ChargepointPrice(
}
}
data class ChargepriceBranding(
@Json(name = "background_color") val backgroundColor: String,
@Json(name = "text_color") val textColor: String,
@Json(name = "logo_url") val logoUrl: String
)
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
val isOnlyKwh =
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
@@ -339,6 +351,19 @@ data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
enum class ChargepriceInclude {
@Json(name = "filter")
FILTER,
@Json(name = "always")
ALWAYS,
@Json(name = "exclusive")
EXCLUSIVE
}
data class ChargepriceRequestTariffMeta(
val include: ChargepriceInclude
)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,

View File

@@ -402,7 +402,7 @@ class GoingElectricApiWrapper(
val chargeCards = referenceData.chargecards
val plugMap = plugs.map { plug ->
plug to nameForPlugType(sp, plug)
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
}.toMap()
val networkMap = networks.map { it to it }.toMap()
val chargecardMap = chargeCards.map { it.id.toString() to it.name }.toMap()
@@ -448,11 +448,11 @@ class GoingElectricApiWrapper(
MultipleChoiceFilter(
sp.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(
commonChoices = listOf(
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.CCS_UNKNOWN,
Chargepoint.CHADEMO
),
).map { GEChargepoint.convertTypeToGE(it)!! }.toSet(),
manyChoices = true
),
SliderFilter(

View File

@@ -87,7 +87,14 @@ data class GECost(
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
) {
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
fun convert() = Cost(
// In GE, freecharging = false can either mean "paid charging" or "no information
// available", only freecharging = true provides useful information. Therefore convert
// false to null. Same for freeparking.
if (freecharging) freecharging else null,
if (freeparking) freeparking else null,
descriptionShort, descriptionLong
)
}
@JsonClass(generateAdapter = true)
@@ -126,7 +133,7 @@ data class GEHours(
val start: LocalTime?,
val end: LocalTime?
) {
fun convert() = Hours(start, end)
fun convert() = if (start != null && end != null) Hours(start, end) else null
}
@JsonClass(generateAdapter = true)

View File

@@ -1,11 +1,15 @@
package net.vonforst.evmap.autocomplete
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.parcelize.Parcelize
interface AutocompleteProvider {
val id: String
fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace>
suspend fun getDetails(id: String): PlaceWithBounds
@@ -20,7 +24,7 @@ data class AutocompletePlace(
val primaryText: CharSequence,
val secondaryText: CharSequence,
val id: String,
val distanceMeters: Int?,
val distanceMeters: Double?,
val types: List<AutocompletePlaceType>
)
@@ -167,7 +171,8 @@ enum class AutocompletePlaceType {
TRAVEL_AGENCY,
UNIVERSITY,
VETERINARY_CARE,
ZOO;
ZOO,
RECENT;
companion object {
fun valueOfOrNull(value: String): AutocompletePlaceType? {
@@ -180,4 +185,5 @@ enum class AutocompletePlaceType {
}
}
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
@Parcelize
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?) : Parcelable

View File

@@ -17,12 +17,13 @@ import com.mapbox.geojson.BoundingBox
import com.mapbox.geojson.Point
import net.vonforst.evmap.R
import java.io.IOException
import kotlin.math.roundToInt
class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
private val bold: CharacterStyle = StyleSpan(Typeface.BOLD)
private val results = HashMap<String, CarmenFeature>()
override val id = "mapbox"
override fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace> {
val result = MapboxGeocoding.builder().apply {
location?.let {
@@ -58,7 +59,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
location?.let { location ->
SphericalUtil.computeDistanceBetween(
feature.center()!!.toLatLng(), location
).roundToInt()
)
},
getPlaceTypes(feature)
)
@@ -112,7 +113,8 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
override fun getAttributionString(): Int = R.string.powered_by_mapbox
override fun getAttributionImage(dark: Boolean): Int = R.drawable.mapbox_logo_icon
override fun getAttributionImage(dark: Boolean): Int =
if (dark) R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
}
private fun BoundingBox.toLatLngBounds(): LatLngBounds {

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -20,15 +21,10 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -84,8 +80,9 @@ class ChargepriceFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val charger = requireArguments().getParcelable<ChargeLocation>(ARG_CHARGER)!!
val dataSource = requireArguments().getString(ARG_DATASOURCE)!!
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
val charger = fragmentArgs.charger
val dataSource = fragmentArgs.dataSource
vm.charger.value = charger
vm.dataSource.value = dataSource
if (vm.chargepoint.value == null) {
@@ -178,6 +175,10 @@ class ChargepriceFragment : DialogFragment() {
dismiss()
true
}
R.id.menu_help -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
else -> false
}
}
@@ -216,28 +217,4 @@ class ChargepriceFragment : DialogFragment() {
)
}
companion object {
const val ARG_CHARGER = "charger"
const val ARG_DATASOURCE = "datasource"
fun showCharger(
charger: ChargeLocation,
dataSource: Class<ChargepointApi<ReferenceData>>
): Bundle {
return Bundle().apply {
putParcelable(
ARG_CHARGER,
charger
)
putString(
ARG_DATASOURCE,
when (dataSource) {
GoingElectricApiWrapper::class.java -> "going_electric"
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
else -> throw IllegalArgumentException("unsupported data source")
}
)
}
}
}
}

View File

@@ -77,7 +77,10 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
onClickListener = {
findNavController().navigate(
R.id.action_favs_to_map,
MapFragment.showCharger(it.charger)
MapFragmentArgs(
chargerId = it.charger.id,
latLng = LatLng(it.charger.coordinates.lat, it.charger.coordinates.lng)
).toBundle()
)
}
}

View File

@@ -43,12 +43,6 @@ class FilterFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
vm.filterProfile.observe(viewLifecycleOwner) {
if (it != null) {
binding.toolbar.title = "${getString(R.string.menu_filter)}: ${it.name}"
}
}
binding.filtersList.apply {
adapter = FiltersAdapter()
layoutManager =
@@ -80,34 +74,52 @@ class FilterFragment : Fragment() {
true
}
R.id.menu_save_profile -> {
showEditTextDialog(requireContext()) { dialog, input ->
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
saveProfile()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun saveProfile(error: Boolean = false) {
showEditTextDialog(requireContext()) { dialog, input ->
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
if (error) {
input.error = getString(R.string.required)
}
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
if (input.text.isBlank()) {
saveProfile(true)
} else {
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
}
override fun onResume() {
super.onResume()
binding.toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
vm.filterProfile.observe(viewLifecycleOwner) {
if (it != null) {
binding.toolbar.title = getString(R.string.edit_filter_profile, it.name)
}
}
}
}

View File

@@ -67,8 +67,8 @@ class FilterProfilesFragment : Fragment() {
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos = viewHolder.adapterPosition;
val toPos = target.adapterPosition;
val fromPos = viewHolder.bindingAdapterPosition;
val toPos = target.bindingAdapterPosition;
val list = vm.filterProfiles.value?.toMutableList()
if (list != null) {

View File

@@ -33,6 +33,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -72,6 +73,7 @@ import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.databinding.FragmentMapBinding
@@ -89,11 +91,6 @@ import net.vonforst.evmap.viewmodel.*
import java.io.IOException
const val ARG_CHARGER_ID = "chargerId"
const val ARG_LAT = "lat"
const val ARG_LON = "lon"
const val ARG_LOCATION_NAME = "locationName"
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
LostApiClient.ConnectionCallbacks, LocationListener {
private lateinit var binding: FragmentMapBinding
@@ -242,6 +239,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
findNavController().navigate(R.id.action_map_to_opensource_donations)
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
} catch (ignored: IllegalStateException) {
// "no current navigation node"
}
}
/*if (!prefs.update060AndroidAutoDialogShown) {
@@ -307,9 +306,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
val dataSource = when (vm.apiType) {
GoingElectricApiWrapper::class.java -> "going_electric"
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
else -> throw IllegalArgumentException("unsupported data source")
}
findNavController().navigate(
R.id.action_map_to_chargepriceFragment,
ChargepriceFragment.showCharger(charger, vm.apiType)
ChargepriceFragmentArgs(charger, dataSource).toBundle()
)
}
binding.detailView.topPart.setOnClickListener {
@@ -368,7 +372,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val place = adapter.getItem(position) ?: return@OnItemClickListener
lifecycleScope.launch {
try {
vm.searchResult.value = adapter.currentProvider!!.getDetails(place.id)
vm.searchResult.value = adapter.getDetails(place.id)
} catch (e: ApiUnavailableException) {
e.printStackTrace()
} catch (e: IOException) {
@@ -388,9 +392,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
binding.search.keyListener = searchKeyListener
if (binding.search.text.isNotEmpty() && isVisible) {
binding.search.showDropDown()
}
} else {
binding.search.keyListener = null
}
@@ -443,11 +444,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
val isFav = favs.find { it.id == charger.id } != null
if (isFav) {
vm.deleteFavorite(charger)
} else {
vm.insertFavorite(charger)
}
markers.inverse[charger]?.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = !isFav
)
)
}
private fun setupObservers() {
@@ -476,6 +487,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
bottomSheetBehavior.state = STATE_COLLAPSED
}
removeSearchFocus()
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle()
@@ -574,7 +586,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -588,7 +601,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
animator.animateMarkerBounce(marker)
@@ -601,7 +615,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -803,10 +818,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val position = vm.mapPosition.value
val lat = arguments?.optDouble(ARG_LAT)
val lon = arguments?.optDouble(ARG_LON)
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
val locationName = arguments?.getString(ARG_LOCATION_NAME)
val fragmentArgs: MapFragmentArgs by navArgs()
val locationName = fragmentArgs.locationName
val chargerId = fragmentArgs.chargerId
val latLng = fragmentArgs.latLng
var positionSet = false
@@ -815,7 +830,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
map.moveCamera(cameraUpdate)
positionSet = true
} else if (chargerId != null && (lat == null || lon == null)) {
} else if (chargerId != 0L && latLng == null) {
// show given charger ID
vm.loadChargerById(chargerId)
vm.chargerSparse.observe(
@@ -833,13 +848,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
positionSet = true
} else if (lat != null && lon != null) {
} else if (latLng != null) {
// show given position
val latLng = LatLng(lat, lon)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
if (chargerId != null) {
if (chargerId != 0L) {
// show charger detail after chargers were loaded
vm.chargepoints.observe(
viewLifecycleOwner,
@@ -896,6 +910,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
if (vm.searchResult.value != null) {
// show search result (after configuration change)
vm.searchResult.postValue(vm.searchResult.value)
}
}
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
@@ -944,7 +963,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -962,7 +982,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav)
} else {
animator.deleteMarker(marker)
}
@@ -977,6 +998,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
@@ -987,12 +1009,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
255,
highlight,
fault,
multi
multi,
fav
)
)
.anchor(0.5f, 1f)
)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi, fav)
markers[marker] = charger
}
}
@@ -1084,6 +1107,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.id.menu_group_filter_profiles,
Menu.NONE, Menu.NONE, R.string.no_filters
)
val favoritesItem = popup.menu.add(
R.id.menu_group_filter_profiles,
Menu.NONE,
Menu.NONE, R.string.filter_favorites
)
profiles.forEach { profile ->
val item = popup.menu.add(
R.id.menu_group_filter_profiles,
@@ -1100,11 +1128,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
profilesMap[FILTERS_DISABLED] = noFiltersItem
profilesMap[FILTERS_CUSTOM] = customItem
profilesMap[FILTERS_FAVORITES] = favoritesItem
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true);
val manageFiltersItem = popup.menu.findItem(R.id.menu_manage_filter_profiles)
manageFiltersItem.isVisible = !profiles.isEmpty()
manageFiltersItem.isVisible = profiles.isNotEmpty()
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
when (id) {
@@ -1116,6 +1145,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
customItem.isVisible = true
customItem.isChecked = true
}
FILTERS_FAVORITES -> {
customItem.isVisible = false
favoritesItem.isChecked = true
}
else -> {
customItem.isVisible = false
val item = profilesMap[id]
@@ -1154,43 +1187,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
companion object {
fun showCharger(charger: ChargeLocation): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, charger.id)
putDouble(ARG_LAT, charger.coordinates.lat)
putDouble(ARG_LON, charger.coordinates.lng)
}
}
fun showLocation(lat: Double, lon: Double): Bundle {
return Bundle().apply {
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showChargerById(id: Long): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
}
}
fun showCharger(id: Long, lat: Double, lon: Double): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showLocationByName(query: String): Bundle {
return Bundle().apply {
putString(ARG_LOCATION_NAME, query)
}
}
}
override fun onConnected() {
val map = this.map ?: return
val context = this.context ?: return

View File

@@ -79,7 +79,14 @@ class MultiSelectDialog : AppCompatDialogFragment() {
items = data.entries.toList()
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
.sortedByDescending { commonChoices?.contains(it.key) == true }
.sortedBy {
when {
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
selected.contains(it.key) -> 1
commonChoices?.contains(it.key) == true -> 2
else -> 3
}
}
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
adapter.submitList(items)

View File

@@ -1,4 +1,4 @@
package net.vonforst.evmap.fragment
package net.vonforst.evmap.fragment.preference
import android.os.Bundle
import androidx.appcompat.widget.Toolbar

View File

@@ -0,0 +1,40 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected lateinit var prefs: PreferenceDataSource
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prefs = PreferenceDataSource(requireContext())
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
val navController = findNavController()
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
}
override fun onPause() {
preferenceManager.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
}

View File

@@ -1,28 +1,15 @@
package net.vonforst.evmap.fragment
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var prefs: PreferenceDataSource
class ChargepriceSettingsFragment : BaseSettingsFragment() {
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
@@ -37,7 +24,6 @@ class SettingsFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prefs = PreferenceDataSource(requireContext())
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
@@ -92,62 +78,21 @@ class SettingsFragment : PreferenceFragmentCompat(),
"${it.brand} ${it.name}"
}.joinToString(", ")
myVehiclePreference.summary = summary
// TODO: prefs.chargepriceMyVehicleDcChargeports = it.dcChargePorts
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
else -> super.onPreferenceTreeClick(preference)
}
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"language" -> {
activity?.let {
it.finish();
it.startActivity(it.intent);
}
}
"darkmode" -> {
updateNightMode(prefs)
}
"chargeprice_my_vehicle" -> {
updateMyVehiclesSummary()
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
"search_provider" -> {
if (prefs.searchProvider == "google") {
Toast.makeText(context, R.string.pref_search_provider_info, Toast.LENGTH_LONG)
.show()
}
}
}
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
val navController = findNavController()
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
}
override fun onPause() {
preferenceManager.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
}

View File

@@ -0,0 +1,63 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.TextView
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.R
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class DataSettingsFragment : BaseSettingsFragment() {
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
)
}
})
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_data, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"search_provider" -> {
if (prefs.searchProvider == "google") {
Snackbar.make(
requireView(),
R.string.pref_search_provider_info,
Snackbar.LENGTH_INDEFINITE
).apply {
setAction(R.string.ok) {}
this.view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
?.apply {
maxLines = 6
}
}
.show()
}
}
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
"search_delete_recent" -> {
Snackbar.make(
requireView(),
R.string.deleted_recent_search_results,
Snackbar.LENGTH_LONG
)
.show()
vm.deleteRecentSearchResults()
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
}

View File

@@ -0,0 +1,22 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import net.vonforst.evmap.R
class SettingsFragment : BaseSettingsFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
}
}

View File

@@ -0,0 +1,26 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.updateNightMode
class UiSettingsFragment : BaseSettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_ui, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"language" -> {
activity?.let {
it.finish();
it.startActivity(it.intent);
}
}
"darkmode" -> {
updateNightMode(prefs)
}
}
}
}

View File

@@ -126,6 +126,9 @@ data class Cost(
val descriptionShort: String? = null,
val descriptionLong: String? = null
) : Parcelable {
val isEmpty: Boolean
get() = descriptionLong == null && descriptionShort == null && freecharging == null && freeparking == null
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
if (freecharging != null && freeparking != null) {
val charging =
@@ -137,6 +140,22 @@ data class Cost(
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
}
} else if (freecharging != null) {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$charging"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_charging, charging), 0)
}
} else if (freeparking != null) {
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$parking"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_parking, parking), 0)
}
} else if (descriptionShort != null) {
return descriptionShort
} else if (descriptionLong != null) {
@@ -162,9 +181,7 @@ data class OpeningHours(
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
} else if (days != null) {
val hours = days.getHoursForDate(LocalDate.now())
if (hours.start == null || hours.end == null) {
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
}
?: return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
val now = LocalTime.now()
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
@@ -192,21 +209,21 @@ data class OpeningHours(
@Parcelize
data class OpeningHoursDays(
@Embedded(prefix = "mo") val monday: Hours,
@Embedded(prefix = "tu") val tuesday: Hours,
@Embedded(prefix = "we") val wednesday: Hours,
@Embedded(prefix = "th") val thursday: Hours,
@Embedded(prefix = "fr") val friday: Hours,
@Embedded(prefix = "sa") val saturday: Hours,
@Embedded(prefix = "su") val sunday: Hours,
@Embedded(prefix = "ho") val holiday: Hours
@Embedded(prefix = "mo") val monday: Hours?,
@Embedded(prefix = "tu") val tuesday: Hours?,
@Embedded(prefix = "we") val wednesday: Hours?,
@Embedded(prefix = "th") val thursday: Hours?,
@Embedded(prefix = "fr") val friday: Hours?,
@Embedded(prefix = "sa") val saturday: Hours?,
@Embedded(prefix = "su") val sunday: Hours?,
@Embedded(prefix = "ho") val holiday: Hours?
) : Parcelable {
fun getHoursForDate(date: LocalDate): Hours {
fun getHoursForDate(date: LocalDate): Hours? {
// TODO: check for holidays
return getHoursForDayOfWeek(date.dayOfWeek)
}
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours? {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
return when (dayOfWeek) {
DayOfWeek.MONDAY -> monday
@@ -223,16 +240,12 @@ data class OpeningHoursDays(
@Parcelize
data class Hours(
val start: LocalTime?,
val end: LocalTime?
val start: LocalTime,
val end: LocalTime
) : Parcelable {
override fun toString(): String {
if (start != null && end != null) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
} else {
return "closed"
}
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
}
}

View File

@@ -129,4 +129,5 @@ fun FilterValues.getMultipleChoiceValue(key: String) =
this.find { it.value.key == key }?.value as MultipleChoiceFilterValue?
const val FILTERS_DISABLED = -2L
const val FILTERS_CUSTOM = -1L
const val FILTERS_CUSTOM = -1L
const val FILTERS_FAVORITES = -3L

View File

@@ -23,4 +23,12 @@ interface ChargeLocationsDao {
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
@Query("SELECT * FROM chargelocation WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getChargeLocationsInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<ChargeLocation>
}

View File

@@ -24,19 +24,21 @@ import net.vonforst.evmap.model.*
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
FilterProfile::class,
RecentAutocompletePlace::class,
GEPlug::class,
GENetwork::class,
GEChargeCard::class,
OCMConnectionType::class,
OCMCountry::class,
OCMOperator::class
], version = 13
], version = 14
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun filterValueDao(): FilterValueDao
abstract fun filterProfileDao(): FilterProfileDao
abstract fun recentAutocompletePlaceDao(): RecentAutocompletePlaceDao
// GoingElectric API specific
abstract fun geReferenceDataDao(): GEReferenceDataDao
@@ -51,7 +53,7 @@ abstract class AppDatabase : RoomDatabase() {
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13
MIGRATION_12, MIGRATION_13, MIGRATION_14
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -303,5 +305,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_14 = object : Migration(13, 14) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))");
}
}
}
}

View File

@@ -54,7 +54,7 @@ abstract class FilterValueDao {
)
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
if (filterStatus == FILTERS_DISABLED) {
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
MutableLiveData(emptyList())
} else {
MediatorLiveData<List<FilterValue>>().apply {

View File

@@ -0,0 +1,70 @@
package net.vonforst.evmap.storage
import androidx.room.*
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.autocomplete.AutocompletePlace
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.utils.distanceBetween
import java.time.Instant
@Entity(primaryKeys = ["id", "dataSource"])
data class RecentAutocompletePlace(
val id: String,
val dataSource: String,
var timestamp: Instant,
val primaryText: String,
val secondaryText: String,
val latLng: LatLng,
val viewport: LatLngBounds?,
val types: List<AutocompletePlaceType>
) {
constructor(
place: AutocompletePlace,
details: PlaceWithBounds,
dataSource: String,
timestamp: Instant
) : this(
place.id, dataSource, timestamp, place.primaryText.toString(),
place.secondaryText.toString(), details.latLng, details.viewport, place.types
)
fun asAutocompletePlace(currentLocation: LatLng?): AutocompletePlace {
return AutocompletePlace(
primaryText,
secondaryText,
id,
currentLocation?.let {
distanceBetween(
latLng.latitude, latLng.longitude,
it.latitude, it.longitude
)
},
types + AutocompletePlaceType.RECENT
)
}
fun asPlaceWithBounds(): PlaceWithBounds {
return PlaceWithBounds(latLng, viewport)
}
}
@Dao
abstract class RecentAutocompletePlaceDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(vararg places: RecentAutocompletePlace)
@Query("DELETE FROM recentautocompleteplace")
abstract suspend fun deleteAll()
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource AND primaryText LIKE '%' || :query || '%' ORDER BY timestamp DESC LIMIT :limit")
abstract fun search(
query: String,
dataSource: String,
limit: Int? = null
): List<RecentAutocompletePlace>
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
abstract fun getAll(dataSource: String, limit: Int? = null): List<RecentAutocompletePlace>
}

View File

@@ -1,11 +1,14 @@
package net.vonforst.evmap.storage
import androidx.room.TypeConverter
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter
import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargerPhoto
@@ -41,6 +44,12 @@ class Converters {
val type = Types.newParameterizedType(List::class.java, String::class.java)
moshi.adapter<List<String>>(type)
}
private val latLngAdapter by lazy {
moshi.adapter<LatLng>(LatLng::class.java)
}
private val latLngBoundsAdapter by lazy {
moshi.adapter<LatLngBounds>(LatLngBounds::class.java)
}
@TypeConverter
fun fromChargepointList(value: List<Chargepoint>?): String {
@@ -115,4 +124,34 @@ class Converters {
fun toStringList(value: String): List<String>? {
return stringListAdapter.fromJson(value)
}
@TypeConverter
fun fromLatLng(value: LatLng?): String {
return latLngAdapter.toJson(value)
}
@TypeConverter
fun toLatLng(value: String): LatLng? {
return latLngAdapter.fromJson(value)
}
@TypeConverter
fun fromLatLngBounds(value: LatLngBounds?): String {
return latLngBoundsAdapter.toJson(value)
}
@TypeConverter
fun toLatLngBounds(value: String): LatLngBounds? {
return latLngBoundsAdapter.fromJson(value)
}
@TypeConverter
fun fromAutocompletePlaceTypeList(value: List<AutocompletePlaceType>): String {
return value.joinToString(",") { it.name }
}
@TypeConverter
fun toAutocompletePlaceTypeList(value: String): List<AutocompletePlaceType> {
return value.split(",").map { AutocompletePlaceType.valueOf(it) }
}
}

View File

@@ -0,0 +1,20 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
class AutocompleteTextViewWithSuggestions(ctx: Context, args: AttributeSet) :
androidx.appcompat.widget.AppCompatAutoCompleteTextView(ctx, args) {
override fun enoughToFilter(): Boolean = true
override fun onFocusChanged(
focused: Boolean, direction: Int,
previouslyFocusedRect: Rect?
) {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
if (focused && adapter != null) {
performFiltering(text, 0)
}
}
}

View File

@@ -2,6 +2,10 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@@ -18,14 +22,12 @@ import androidx.databinding.InverseBindingListener
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import coil.load
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.R
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.meterPerFt
import net.vonforst.evmap.shouldUseImperialUnits
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -344,18 +346,68 @@ fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}
@BindingAdapter("myTariffsBackground")
fun myTariffsBackground(view: View, myTariff: Boolean) {
if (myTariff) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
} else {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?): Drawable? {
when {
myTariff -> {
return ContextCompat.getDrawable(context, R.drawable.my_tariff_background)
}
brandingColor != null -> {
val drawable = ContextCompat.getDrawable(context, R.drawable.branded_tariff_background)
val color = colorToTransparent(Color.parseColor(brandingColor))
(drawable as LayerDrawable).setDrawableByLayerId(
R.id.background, ColorDrawable(
color
)
)
return drawable
}
else -> {
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
return it.getDrawable(0)
}
}
}
}
fun isDarkMode(context: Context) = context.isDarkMode()
/**
* Converts an opaque color to a transparent color, assuming it was on a white background
* with a certain opacity targetAlpha.
*/
private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int {
if (Color.alpha(color) != 255) return color
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
}
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
if (url != null) {
view.load(url)
} else {
view.setImageDrawable(null)
}
}
@BindingAdapter("tooltipTextCompat")
fun setTooltipTextCompat(view: View, text: String) {
TooltipCompat.setTooltipText(view, text)
}
@BindingAdapter("tintNullable")
fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
if (tint != null) {
view.imageTintList = ColorStateList.valueOf(tint)
} else {
view.imageTintList = null
}
}

View File

@@ -55,7 +55,8 @@ class ChargerIconGenerator(
val alpha: Int,
val highlight: Boolean,
val fault: Boolean,
val multi: Boolean
val multi: Boolean,
val fav: Boolean
)
// 230 items: (21 sizes, 5 colors, multi on/off) + highlight + fault (only with scale = 1)
@@ -66,6 +67,7 @@ class ChargerIconGenerator(
private val highlightIcon = R.drawable.ic_map_marker_highlight
private val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
private val faultIcon = R.drawable.ic_map_marker_fault
private val favIcon = R.drawable.ic_map_marker_fav
fun preloadCache() {
// pre-generates images for scale from 0 to 255 for all possible tint colors
@@ -79,12 +81,14 @@ class ChargerIconGenerator(
for (fault in listOf(false, true)) {
for (highlight in listOf(false, true)) {
for (multi in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi
)
for (fav in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi, fav
)
}
}
}
}
@@ -98,14 +102,16 @@ class ChargerIconGenerator(
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
multi: Boolean = false,
fav: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
multi,
if (scale == 1f) fav else false
)
val cachedImg = cache[data]
return if (cachedImg != null) {
@@ -124,14 +130,16 @@ class ChargerIconGenerator(
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
multi: Boolean = false,
fav: Boolean = false
): Bitmap {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
multi,
if (scale == 1f) fav else false,
)
return generateBitmap(data)
}
@@ -200,6 +208,22 @@ class ChargerIconGenerator(
faultDrawable.draw(canvas)
}
if (data.fav) {
val favDrawable = ContextCompat.getDrawable(context, favIcon)!!
val favSize = 0.75
val favShiftY = 0.25
val favShiftX = if (data.fault) -0.5 else 0.25
val base = width
favDrawable.setBounds(
(leftPadding.toInt() + base * (1 - favSize + favShiftX)).toInt(),
(topPadding.toInt() - base * favShiftY).toInt(),
(leftPadding.toInt() + base * (1 + favShiftX)).toInt(),
(topPadding.toInt() + base * (favSize - favShiftY)).toInt()
)
favDrawable.alpha = data.alpha
favDrawable.draw(canvas)
}
return bm
}
}

View File

@@ -29,7 +29,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
tint: Int,
highlight: Boolean,
fault: Boolean,
multi: Boolean
multi: Boolean,
fav: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -47,7 +48,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
scale = scale,
highlight = highlight,
fault = fault,
multi = multi
multi = multi,
fav = fav
)
)
}
@@ -66,7 +68,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
tint: Int,
highlight: Boolean,
fault: Boolean,
multi: Boolean
multi: Boolean,
fav: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -84,7 +87,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
scale = scale,
highlight = highlight,
fault = fault,
multi = multi
multi = multi,
fav = fav
)
)
}

View File

@@ -4,7 +4,10 @@ import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
@@ -100,7 +103,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataSource,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors
vehicleCompatibleConnectors,
myTariffs, myTariffsAll
).forEach {
addSource(it) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
@@ -208,7 +212,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
val dataSource = dataSource.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null) {
val myTariffs = myTariffs.value
val myTariffsAll = myTariffsAll.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffs == null || myTariffsAll == null) {
chargePrices.value = Resource.error(null, null)
return
}
@@ -222,6 +228,19 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataAdapter = dataSource
station = cpStation
vehicle = HasOne(car)
tariffs = if (!myTariffsAll) {
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,

View File

@@ -42,7 +42,7 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
MediatorLiveData<FilterProfile>().apply {
addSource(filterStatus) { id ->
when (id) {
FILTERS_CUSTOM, FILTERS_DISABLED -> value = null
FILTERS_CUSTOM, FILTERS_DISABLED, FILTERS_FAVORITES -> value = null
else -> viewModelScope.launch {
value = db.filterProfileDao().getProfileById(id, prefs.dataSource)
}

View File

@@ -1,12 +1,14 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import android.os.Parcelable
import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
@@ -23,10 +25,12 @@ import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
@@ -38,7 +42,8 @@ internal fun getClusterDistance(zoom: Float): Int? {
}
}
class MapViewModel(application: Application) : AndroidViewModel(application) {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.javaClass
val apiName: String
@@ -49,11 +54,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
val bottomSheetState: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
state.getLiveData("bottomSheetState")
}
val mapPosition: MutableLiveData<MapPosition> by lazy {
MutableLiveData<MapPosition>()
state.getLiveData("mapPosition")
}
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
@@ -124,7 +129,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
state.getLiveData("chargerSparse")
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
@@ -222,7 +227,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
MutableLiveData<PlaceWithBounds>()
state.getLiveData("searchResult")
}
val mapType: MutableLiveData<AnyMap.Type> by lazy {
@@ -296,20 +301,32 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
val mapPosition = data.first
val filters = data.second
val api = api
val refData = data.third
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
val b = mapPosition.bounds
var chargers = db.chargeLocationsDao().getChargeLocationsInBoundsAsync(
b.southwest.latitude,
b.northeast.latitude,
b.southwest.longitude,
b.northeast.longitude
) as List<ChargepointListItem>
val clusterDistance = getClusterDistance(mapPosition.zoom)
clusterDistance?.let {
chargers = cluster(chargers, mapPosition.zoom, clusterDistance)
}
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
chargepoints.value = Resource.success(chargers)
return@throttleLatest
}
chargepoints.value = result
if (api is GoingElectricApiWrapper) {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
@@ -333,7 +350,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
}
chargepoints.value = result
}
private suspend fun loadAvailability(charger: ChargeLocation) {

View File

@@ -8,11 +8,13 @@ import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.storage.AppDatabase
import java.io.IOException
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey)
private var db = AppDatabase.getInstance(application)
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
@@ -49,4 +51,10 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
}
}
}
fun deleteRecentSearchResults() {
viewModelScope.launch {
db.recentAutocompletePlaceDao().deleteAll()
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/background"
android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -1,4 +1,5 @@
<vector android:height="15.811624dp" android:viewportHeight="131.5"
android:tint="?attr/colorControlNormal"
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4081"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#000"
android:pathData="M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C12.5,11 13,10.97 13.5,10.92V9.5H16.39L15.39,8.5L18.9,5C17.5,3.8 14.94,3 12,3M18.92,7.08L17.5,8.5L20,11H15V13H20L17.5,15.5L18.92,16.92L23.84,12M4,9V12C4,14.21 7.58,16 12,16C13.17,16 14.26,15.85 15.25,15.63L16.38,14.5H13.5V12.92C13,12.97 12.5,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C14.94,21 17.5,20.2 18.9,19L17,17.1C15.61,17.66 13.9,18 12,18C7.58,18 4,16.21 4,14Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,14c-1.66,0 -3,1.34 -3,3 0,1.31 -1.16,2 -2,2 0.92,1.22 2.49,2 4,2 2.21,0 4,-1.79 4,-4 0,-1.66 -1.34,-3 -3,-3zM20.71,4.63l-1.34,-1.34c-0.39,-0.39 -1.02,-0.39 -1.41,0L9,12.25 11.75,15l8.96,-8.96c0.39,-0.39 0.39,-1.02 0,-1.41z" />
</vector>

View File

@@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="160dp"
android:height="160dp"
android:viewportWidth="120"
android:viewportHeight="120">
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group android:name="_R_G">
<group
android:name="_R_G_L_2_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_2_G"
android:pivotX="53.625"
@@ -43,8 +43,8 @@
</group>
<group
android:name="_R_G_L_1_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_1_G"
android:translateX="-26.049"
@@ -77,8 +77,8 @@
</group>
<group
android:name="_R_G_L_0_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_0_G"
android:translateX="-1.3999999999999995"

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="81dp"
android:height="20dp"
android:viewportWidth="81"
android:viewportHeight="20">
<path
android:fillColor="?colorControlNormal"
android:pathData="M9.1,0.9C4.1,0.9 0,5 0,10s4.1,9.1 9.1,9.1s9.1,-4.1 9.1,-9.1S14.1,0.9 9.1,0.9zM55.7,2.9c-0.1,0 -0.2,0.1 -0.2,0.2v10.5c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2v-0.7c0.7,0.7 1.7,1.2 2.7,1.2c2.1,0 3.9,-1.8 3.9,-4.1S62.1,5.9 60,5.9c-1,0 -2,0.4 -2.7,1.2V3.1c0,-0.1 -0.1,-0.2 -0.2,-0.2H55.7zM10,4.4c1.2,0 2.3,0.5 3.2,1.4c1.8,1.8 1.9,4.7 0.1,6.4c-3.1,3.1 -8.6,2.1 -8.6,2.1s-1,-5.5 2.1,-8.6C7.7,4.8 8.9,4.4 10,4.4zM26.4,5.9c-0.8,0 -1.6,0.4 -2.1,1.1V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2v7.4c0,0.1 0.1,0.2 0.2,0.2H24c0.1,0 0.2,-0.1 0.2,-0.2V9.3c0.1,-1 0.7,-1.8 1.6,-1.8c0.9,0 1.6,0.7 1.6,1.7v4.5c0,0.1 0.1,0.2 0.2,0.2H29c0.1,0 0.2,-0.1 0.2,-0.2l0,-4.6c0.1,-0.9 0.8,-1.6 1.6,-1.6c0.9,0 1.6,0.7 1.6,1.7v4.5c0,0.1 0.1,0.2 0.2,0.2H34c0.1,0 0.2,-0.1 0.2,-0.2l0,-5.1c0,-1.5 -1.3,-2.7 -2.8,-2.7c-1,0 -2,0.6 -2.4,1.5C28.4,6.5 27.4,5.9 26.4,5.9L26.4,5.9zM39.4,5.9c-2.1,0 -3.9,1.8 -3.9,4.1s1.7,4.1 3.9,4.1c1,0 2,-0.4 2.7,-1.2v0.7c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2V7C41.4,6.3 40.4,5.9 39.4,5.9zM50.2,5.9c-1,0 -2,0.4 -2.7,1.2V6.3c0,-0.1 -0.1,-0.2 -0.2,-0.2h-1.4c-0.1,0 -0.2,0.1 -0.2,0.2v10.5c0,0.1 0.1,0.2 0.2,0.2h1.4c0.1,0 0.2,-0.1 0.2,-0.2v-3.9c0.7,0.7 1.7,1.2 2.7,1.2c2.1,0 3.9,-1.8 3.9,-4.1S52.3,5.9 50.2,5.9zM69.1,5.9c-2.3,0 -4.2,1.8 -4.2,4.1s1.9,4.1 4.2,4.1c2.3,0 4.2,-1.8 4.2,-4.1S71.4,5.9 69.1,5.9zM73.7,6.1c-0.1,0 -0.2,0.1 -0.2,0.2c0,0 0,0.1 0,0.1l2.3,3.6l-2.4,3.6c-0.1,0.1 0,0.2 0.1,0.3c0,0 0.1,0 0.1,0h1.6c0.1,0 0.2,-0.1 0.3,-0.2l1.4,-2.4l1.4,2.4c0.1,0.1 0.2,0.2 0.3,0.2h1.6c0.1,0 0.2,-0.1 0.2,-0.2c0,0 0,-0.1 0,-0.1L78.1,10l2.3,-3.6c0.1,-0.1 0,-0.2 -0.1,-0.3c0,0 -0.1,0 -0.1,0h-1.6c-0.1,0 -0.2,0.1 -0.3,0.2L77,8.6l-1.4,-2.3c-0.1,-0.1 -0.2,-0.2 -0.3,-0.2H73.7zM10.1,6.2L9.2,8.1L7.4,8.9l1.8,0.9l0.9,1.8L11,9.8l1.8,-0.9L11,8.1L10.1,6.2zM39.7,7.5c1.3,0 2.3,1.1 2.4,2.4V10c0,1.3 -1.1,2.4 -2.4,2.4c-1.3,0 -2.4,-1.1 -2.4,-2.5S38.4,7.5 39.7,7.5L39.7,7.5zM49.9,7.5c1.3,0 2.4,1.1 2.4,2.5s-1.1,2.5 -2.4,2.5c-1.3,0 -2.3,-1.1 -2.4,-2.4V10C47.5,8.6 48.6,7.5 49.9,7.5L49.9,7.5zM59.7,7.5C61,7.5 62,8.6 62,10s-1.1,2.5 -2.4,2.5c-1.3,0 -2.3,-1.1 -2.4,-2.4V10C57.3,8.6 58.4,7.5 59.7,7.5L59.7,7.5zM69.1,7.5c1.3,0 2.4,1.1 2.4,2.5s-1.1,2.5 -2.4,2.5c-1.3,0 -2.4,-1.1 -2.4,-2.5S67.8,7.5 69.1,7.5z" />
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -10,7 +10,8 @@
android:name="net.vonforst.evmap.navigation.NavHostFragment"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true" />
android:fitsSystemWindows="true"
app:defaultNavHost="true" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"

View File

@@ -317,7 +317,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/go_to_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargepriceData != null &amp;&amp; charger.data.chargepriceData.country != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:icon="@drawable/ic_chargeprice"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"

View File

@@ -69,7 +69,7 @@
android:focusable="true"
android:focusableInTouchMode="true">
<AutoCompleteTextView
<net.vonforst.evmap.ui.AutocompleteTextViewWithSuggestions
android:id="@+id/search"
android:layout_width="0dp"
android:layout_weight="1"

View File

@@ -37,17 +37,16 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
<TextView
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.tariffName}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
@@ -57,12 +56,11 @@
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Cheap Charging Co." />
@@ -71,11 +69,10 @@
android:id="@+id/rvTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:nestedScrollingEnabled="false"
app:data="@{item.tags}"
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
tools:itemCount="1"
@@ -85,12 +82,12 @@
android:id="@+id/txtProviderCustomerTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/chargeprice_provider_customer_tariff"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.providerCustomerTariff}"
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
@@ -98,12 +95,11 @@
android:id="@+id/txtMonthlyFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.formatMonthlyFees(context)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
tools:text="Base fee 1 €/month" />
@@ -160,5 +156,19 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.65" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:scaleType="fitCenter"
app:goneUnless="@{item.branding.logoUrl != null}"
app:imageUrl="@{item.branding.logoUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -37,7 +37,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals(&quot;closed&quot;) ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek) == null ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -2,6 +2,12 @@
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
<item
android:id="@+id/menu_close"
android:icon="@drawable/ic_close"

View File

@@ -33,10 +33,24 @@
<action
android:id="@+id/action_map_to_opensource_donations"
app:destination="@id/opensource_donations" />
<argument
android:name="locationName"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="chargerId"
android:defaultValue="0L"
app:argType="long" />
<argument
android:name="latLng"
android:defaultValue="@null"
app:argType="com.car2go.maps.model.LatLng"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/about"
android:name="net.vonforst.evmap.fragment.AboutFragment"
android:name="net.vonforst.evmap.fragment.preference.AboutFragment"
android:label="@string/about"
tools:layout="@layout/fragment_preference">
<action
@@ -48,9 +62,24 @@
</fragment>
<fragment
android:id="@+id/settings"
android:name="net.vonforst.evmap.fragment.SettingsFragment"
android:name="net.vonforst.evmap.fragment.preference.SettingsFragment"
android:label="@string/settings"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_ui"
android:name="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
android:label="@string/settings_ui"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_data"
android:name="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:label="@string/settings_data_sources"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_chargeprice"
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:label="@string/settings_chargeprice"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/favs"
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
@@ -82,6 +111,12 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
<argument
android:name="dataSource"
app:argType="string" />
</dialog>
<fragment
android:id="@+id/donate"

View File

@@ -17,6 +17,8 @@
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
<string name="cost">Kosten</string>
<string name="cost_detail"><![CDATA[<b>Laden:</b> %s · <b>Parken:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
<string name="free">Kostenlos</string>
<string name="paid">Kostenpflichtig</string>
<string name="amenities">Ladeweile</string>
@@ -143,6 +145,7 @@
<string name="menu_save_profile">Als Profil speichern</string>
<string name="no_filters">Keine Filter</string>
<string name="filter_custom">Verändertes Filterprofil</string>
<string name="filter_favorites">Favoriten</string>
<string name="reorder">Reihenfolge ändern</string>
<string name="delete">Löschen</string>
<string name="save_as_profile">Als Profil speichern</string>
@@ -191,7 +194,7 @@
<string name="chargeprice_vehicle">Fahrzeug</string>
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_title">Preisvergleich</string>
<string name="chargeprice_title">Preise</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="pref_chargeprice_currency">Währung</string>
@@ -238,4 +241,13 @@
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
<string name="unnamed_filter_profile">Unbenanntes Filterprofil</string>
<string name="privacy_link">https://evmap.vonforst.net/de/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/de/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/de/chargeprice_faq.html</string>
<string name="required">erforderlich</string>
<string name="edit_filter_profile">„%s“ bearbeiten</string>
<string name="pref_search_delete_recent">Suchverlauf löschen</string>
<string name="deleted_recent_search_results">Suchverlauf wurde gelöscht</string>
<string name="settings_data_sources">Datenquellen</string>
<string name="help">Hilfe</string>
</resources>

View File

@@ -2,8 +2,6 @@
<resources>
<string name="shared_element_picture">picture</string>
<string name="github_link">https://github.com/johan12345/EVMap</string>
<string name="privacy_link">https://evmap.vonforst.net/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/faq.html</string>
<string name="twitter_handle">\@ev_map</string>
<string name="twitter_url">https://twitter.com/ev_map</string>
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>

View File

@@ -16,6 +16,8 @@
<string name="holiday">Holiday</string>
<string name="cost">Cost</string>
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s charging</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parking</b>]]></string>
<string name="free">Free</string>
<string name="paid">Paid</string>
<string name="amenities">Amenities</string>
@@ -142,6 +144,7 @@
<string name="menu_save_profile">Save as profile</string>
<string name="no_filters">No filters</string>
<string name="filter_custom">Modified filter</string>
<string name="filter_favorites">Favorites</string>
<string name="reorder">reorder</string>
<string name="delete">Delete</string>
<string name="save_as_profile">Save as profile</string>
@@ -223,4 +226,13 @@
<string name="donate_desc">Support EVMap\'s development with a one-time donation</string>
<string name="github_sponsors_desc">Support EVMap on GitHub Sponsors</string>
<string name="unnamed_filter_profile">Unnamed filter profile</string>
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="required">required</string>
<string name="edit_filter_profile">Edit “%s”</string>
<string name="pref_search_delete_recent">Delete recent search results</string>
<string name="deleted_recent_search_results">Recent search results have been deleted</string>
<string name="settings_data_sources">Data sources</string>
<string name="help">Help</string>
</resources>

View File

@@ -1,87 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_ui">
<ListPreference
android:key="language"
android:title="@string/pref_language"
android:entries="@array/pref_language_names"
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
<ListPreference
android:key="darkmode"
android:title="@string/pref_darkmode"
android:entries="@array/pref_darkmode_names"
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_charger_data">
<net.vonforst.evmap.ui.DataSourceSelectDialogPreference
android:key="data_source"
android:title="@string/pref_data_source"
android:entries="@array/pref_data_source_names"
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">
<ListPreference
android:key="map_provider"
android:title="@string/pref_map_provider"
android:entries="@array/pref_map_provider_names"
android:entryValues="@array/pref_map_provider_values"
android:defaultValue="@string/pref_map_provider_default"
android:summary="%s" />
<ListPreference
android:key="search_provider"
android:title="@string/pref_search_provider"
android:entries="@array/pref_search_provider_names"
android:entryValues="@array/pref_search_provider_values"
android:defaultValue="@string/pref_search_provider_default"
android:summary="%s" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_chargeprice">
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"
app:showAllButton="false"
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs"
android:summary="@string/pref_my_tariffs_summary" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceCategory>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
android:title="@string/settings_ui"
android:icon="@drawable/ic_settings_ui" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:title="@string/settings_data_sources"
android:icon="@drawable/ic_settings_data_source" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:title="@string/settings_chargeprice"
android:icon="@drawable/ic_chargeprice" />
</PreferenceScreen>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"
app:showAllButton="false"
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs"
android:summary="@string/pref_my_tariffs_summary" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceScreen>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_charger_data">
<net.vonforst.evmap.ui.DataSourceSelectDialogPreference
android:key="data_source"
android:title="@string/pref_data_source"
android:entries="@array/pref_data_source_names"
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">
<ListPreference
android:key="map_provider"
android:title="@string/pref_map_provider"
android:entries="@array/pref_map_provider_names"
android:entryValues="@array/pref_map_provider_values"
android:defaultValue="@string/pref_map_provider_default"
android:summary="%s" />
<ListPreference
android:key="search_provider"
android:title="@string/pref_search_provider"
android:entries="@array/pref_search_provider_names"
android:entryValues="@array/pref_search_provider_values"
android:defaultValue="@string/pref_search_provider_default"
android:summary="%s" />
<Preference
android:key="search_delete_recent"
android:title="@string/pref_search_delete_recent" />
</PreferenceCategory>
</PreferenceScreen>

Some files were not shown because too many files have changed in this diff Show More