Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90254915e3 | ||
|
|
b7f56ecff4 | ||
|
|
fa3910d3c8 | ||
|
|
4500c55560 | ||
|
|
a493e1a548 | ||
|
|
ddaab42e45 | ||
|
|
9f50341ab7 | ||
|
|
9966b44a76 | ||
|
|
d44b2206d2 | ||
|
|
f61082f491 | ||
|
|
f58d96c939 | ||
|
|
29aedfa3d9 | ||
|
|
8331f92f10 | ||
|
|
123680d3e8 | ||
|
|
0f6b45d745 | ||
|
|
69faa94f18 | ||
|
|
70805b7960 | ||
|
|
56453b0658 | ||
|
|
975d95e37e | ||
|
|
ba34cd016a | ||
|
|
590b16aa49 | ||
|
|
5fe8d0cab4 | ||
|
|
9d7b181410 | ||
|
|
128532aac6 | ||
|
|
486854f56c | ||
|
|
1e30db5cd1 | ||
|
|
aad386ab04 | ||
|
|
e2bcf8d1cd | ||
|
|
f56fad1282 | ||
|
|
adb4d938cc | ||
|
|
b773f65912 | ||
|
|
de335b18d8 | ||
|
|
6c8380b8ce | ||
|
|
81afdca19d | ||
|
|
14e03ba6dd | ||
|
|
abe12b45c3 | ||
|
|
23387ae371 | ||
|
|
25f466b6d7 | ||
|
|
6692b21bf9 | ||
|
|
5959fe8be4 | ||
|
|
00f4c13fcc | ||
|
|
47054d470b | ||
|
|
d10192cae1 | ||
|
|
e1b90955c3 | ||
|
|
d249bf47c7 | ||
|
|
738dcd5f8d | ||
|
|
ad4f32ec32 | ||
|
|
4d03107ae7 | ||
|
|
0e93e310bf | ||
|
|
6cb8940696 | ||
|
|
dad30eb51e | ||
|
|
abf6a2b933 | ||
|
|
2c5685d918 | ||
|
|
b61e57b022 | ||
|
|
e6428cc8db | ||
|
|
6302006a35 | ||
|
|
ab93577a98 | ||
|
|
98b695ed4b | ||
|
|
ed8cb50b08 | ||
|
|
88d89c2760 | ||
|
|
80c25cb416 | ||
|
|
81c8e54dd2 | ||
|
|
8c01ee1581 | ||
|
|
e8db5acfbf | ||
|
|
f6bb3c03ba | ||
|
|
134f3856b9 | ||
|
|
4974cc6d83 | ||
|
|
edd072b83a | ||
|
|
35ddda5bfe | ||
|
|
8b241e3f6f | ||
|
|
b3c5fe788d | ||
|
|
6fd737f6e9 | ||
|
|
08cd4eb849 | ||
|
|
ff75594b37 | ||
|
|
2576bc4854 | ||
|
|
b2c29b647b | ||
|
|
2167a63321 | ||
|
|
fb0a2cfa1c | ||
|
|
07be77c573 | ||
|
|
ae0a84db4c | ||
|
|
dc5ffb148d | ||
|
|
066b7c085e | ||
|
|
4ae16df064 | ||
|
|
17a40127e6 | ||
|
|
31ad748796 | ||
|
|
fe4db38798 | ||
|
|
6c2243078b | ||
|
|
71f1ee8d7b | ||
|
|
ab0c37cb82 | ||
|
|
65189cd798 | ||
|
|
630178bfcf | ||
|
|
bcee975124 | ||
|
|
04fc17d73c | ||
|
|
139c02ef70 | ||
|
|
88a8520f27 | ||
|
|
4f3157a0ac | ||
|
|
17d57729b3 | ||
|
|
1f3df2e0bf | ||
|
|
e2e95ce85d | ||
|
|
d79b554dcc | ||
|
|
98e91ea3db | ||
|
|
b8c8245978 | ||
|
|
fd1f05888a |
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
_img/screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
fastlane/metadata/android/**/images/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
44
README.md
@@ -1,4 +1,4 @@
|
||||
EVMap [](https://travis-ci.org/johan12345/EVMap)
|
||||
EVMap [](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://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/01_main.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/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.
|
||||
|
||||
BIN
_img/screenshots/android_auto/de/11_android_auto_map.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
_img/screenshots/android_auto/de/12_android_auto_detail.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
_img/screenshots/android_auto/de/13_android_auto_prices.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
_img/screenshots/android_auto/de/14_vehicle_data.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
_img/screenshots/android_auto/en/11_android_auto_map.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
_img/screenshots/android_auto/en/12_android_auto_detail.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
_img/screenshots/android_auto/en/13_android_auto_prices.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
_img/screenshots/android_auto/en/14_vehicle_data.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 909 KiB |
|
Before Width: | Height: | Size: 943 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 158 KiB |
BIN
_img/screenshots/phone/de/google/01_map.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
_img/screenshots/phone/de/google/02_detail.png
Normal file
|
After Width: | Height: | Size: 972 KiB |
BIN
_img/screenshots/phone/de/google/03_prices.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
_img/screenshots/phone/de/google/04_favorites.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
_img/screenshots/phone/de/google/05_filters.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
_img/screenshots/phone/de/mapbox/01_map.png
Normal file
|
After Width: | Height: | Size: 875 KiB |
BIN
_img/screenshots/phone/de/mapbox/02_detail.png
Normal file
|
After Width: | Height: | Size: 837 KiB |
BIN
_img/screenshots/phone/de/mapbox/03_prices.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
_img/screenshots/phone/de/mapbox/04_favorites.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
_img/screenshots/phone/de/mapbox/05_filters.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
_img/screenshots/phone/en/google/01_map.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
_img/screenshots/phone/en/google/02_detail.png
Normal file
|
After Width: | Height: | Size: 972 KiB |
BIN
_img/screenshots/phone/en/google/03_prices.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
_img/screenshots/phone/en/google/04_favorites.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
_img/screenshots/phone/en/google/05_filters.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
_img/screenshots/phone/en/mapbox/01_map.png
Normal file
|
After Width: | Height: | Size: 864 KiB |
BIN
_img/screenshots/phone/en/mapbox/02_detail.png
Normal file
|
After Width: | Height: | Size: 837 KiB |
BIN
_img/screenshots/phone/en/mapbox/03_prices.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
_img/screenshots/phone/en/mapbox/04_favorites.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
_img/screenshots/phone/en/mapbox/05_filters.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
@@ -6,15 +6,15 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 57
|
||||
versionName "0.9.1"
|
||||
targetSdkVersion 31
|
||||
versionCode 65
|
||||
versionName "1.1.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -89,6 +89,9 @@ android {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
}
|
||||
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
|
||||
if (mapboxKey == null && project.hasProperty("MAPBOX_API_KEY_ENCRYPTED")) {
|
||||
mapboxKey = decode(project.findProperty("MAPBOX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (mapboxKey != null) {
|
||||
variant.resValue "string", "mapbox_key", mapboxKey
|
||||
}
|
||||
@@ -103,17 +106,17 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation "androidx.activity:activity-ktx:1.2.3"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
|
||||
implementation "androidx.activity:activity-ktx:1.3.1"
|
||||
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.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
@@ -128,7 +131,7 @@ dependencies {
|
||||
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
implementation 'com.airbnb.android:lottie:4.1.0'
|
||||
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
|
||||
implementation 'com.mapzen.android:lost:3.0.2'
|
||||
implementation 'com.google.guava:guava:29.0-android'
|
||||
@@ -136,35 +139,22 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
googleImplementation 'androidx.car.app:app:1.0.0'
|
||||
googleImplementation 'androidx.car.app:app:1.1.0-beta01'
|
||||
googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
|
||||
|
||||
// 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"
|
||||
@@ -195,12 +185,12 @@ 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'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.12.0"
|
||||
|
||||
|
||||
107
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
@@ -5,8 +5,14 @@
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.car.app" />
|
||||
<uses-sdk tools:overrideLibrary="androidx.car.app,androidx.car.app.projected" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
</queries>
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
@@ -21,6 +27,10 @@
|
||||
android:name="androidx.car.app.theme"
|
||||
android:resource="@style/CarAppTheme" />
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="1" />
|
||||
|
||||
<service
|
||||
android:name=".auto.CarAppService"
|
||||
android:label="@string/app_name"
|
||||
@@ -36,8 +46,7 @@
|
||||
<service
|
||||
android:name=".auto.CarLocationService"
|
||||
android:foregroundServiceType="location"
|
||||
android:enabled="true" />
|
||||
|
||||
<activity android:name=".auto.PermissionActivity" />
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -5,10 +5,12 @@ 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
|
||||
|
||||
fun init(context: Context) {
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key));
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key))
|
||||
MapsInitializer.initialize(context.applicationContext, MapsInitializer.Renderer.LATEST, null)
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.IBinder
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.CarHardwareLocation
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
|
||||
|
||||
interface LocationAwareScreen {
|
||||
fun updateLocation(location: Location)
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class CarAppService : androidx.car.app.CarAppService() {
|
||||
override fun createHostValidator(): HostValidator {
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
@@ -38,7 +39,6 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
@@ -47,6 +47,9 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
}
|
||||
private var location: Location? = null
|
||||
private var locationService: CarLocationService? = null
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, ibinder: IBinder) {
|
||||
@@ -59,40 +62,53 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
locationService = null
|
||||
}
|
||||
}
|
||||
private var serviceBound = false
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
return if (locationPermissionGranted()) {
|
||||
WelcomeScreen(carContext, this)
|
||||
} else {
|
||||
PermissionScreen(carContext, this)
|
||||
}
|
||||
return WelcomeScreen(carContext, this)
|
||||
}
|
||||
|
||||
private fun locationPermissionGranted() =
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
fun locationPermissionGranted() = carContext.checkAnyLocationPermission()
|
||||
|
||||
private val locationReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
|
||||
val mapScreen = this@EVMapSession.mapScreen
|
||||
if (location != null && mapScreen != null) {
|
||||
mapScreen.updateLocation(location)
|
||||
}
|
||||
this@EVMapSession.location = location
|
||||
updateLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLocation(location: Location?) {
|
||||
val mapScreen = mapScreen
|
||||
if (location != null && mapScreen != null) {
|
||||
mapScreen.updateLocation(location)
|
||||
}
|
||||
this.location = location
|
||||
}
|
||||
|
||||
private fun onCarHardwareLocationReceived(loc: CarHardwareLocation) {
|
||||
updateLocation(loc.location.value)
|
||||
|
||||
// we successfully received a location from the car hardware,
|
||||
// so we don't need the smartphone location anymore.
|
||||
unbindLocationService()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun bindLocationService() {
|
||||
if (!locationPermissionGranted()) return
|
||||
cas.bindService(
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carSensors.addCarHardwareLocationListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCarHardwareLocationReceived
|
||||
)
|
||||
}
|
||||
serviceBound = cas.bindService(
|
||||
Intent(cas, CarLocationService::class.java),
|
||||
serviceConnection,
|
||||
Context.BIND_AUTO_CREATE
|
||||
@@ -100,10 +116,18 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
private fun onStop() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
hardwareMan.carSensors.removeCarHardwareLocationListener(::onCarHardwareLocationReceived)
|
||||
}
|
||||
unbindLocationService()
|
||||
}
|
||||
|
||||
private fun unbindLocationService() {
|
||||
locationService?.let { service ->
|
||||
service.removeLocationUpdates()
|
||||
locationService?.removeLocationUpdates()
|
||||
if (serviceBound) {
|
||||
cas.unbindService(serviceConnection)
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
263
app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt
Normal file
@@ -0,0 +1,263 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
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
|
||||
|
||||
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private val maxRows = 6
|
||||
private var errorMessage: String? = null
|
||||
private val batteryRange = listOf(20.0, 80.0)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (prices == null) loadData()
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
R.string.chargeprice_battery_range,
|
||||
batteryRange[0],
|
||||
batteryRange[1]
|
||||
) + " · " + carContext.getString(R.string.powered_by_chargeprice)
|
||||
)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (prices == null && errorMessage == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.take(maxRows)?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
}.build())
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
).setOnClickListener {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ContextCompat.getColor(
|
||||
carContext,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build().intent
|
||||
intent.data =
|
||||
Uri.parse("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter()}")
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.no_browser_app_found,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatProvider(price: ChargePrice): String {
|
||||
if (!price.tariffName.startsWith(price.provider)) {
|
||||
return price.provider + " " + price.tariffName
|
||||
} else {
|
||||
return price.tariffName
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPrice(price: ChargePrice): String {
|
||||
val totalPrice = carContext.getString(
|
||||
R.string.charge_price_format,
|
||||
price.chargepointPrices.first().price,
|
||||
currency(price.currency)
|
||||
)
|
||||
val kwhPrice = if (price.chargepointPrices.first().price > 0f) {
|
||||
carContext.getString(
|
||||
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
|
||||
R.string.charge_price_kwh_format
|
||||
} else {
|
||||
R.string.charge_price_average_format
|
||||
},
|
||||
price.chargepointPrices.get(0).price / meta!!.energy,
|
||||
currency(price.currency)
|
||||
)
|
||||
} else null
|
||||
val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
|
||||
price.formatMonthlyFees(carContext)
|
||||
} else null
|
||||
var text = totalPrice
|
||||
if (kwhPrice != null && monthlyFees != null) {
|
||||
text += " ($kwhPrice, $monthlyFees)"
|
||||
} else if (kwhPrice != null) {
|
||||
text += " ($kwhPrice)"
|
||||
} else if (monthlyFees != null) {
|
||||
text += " ($monthlyFees)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
val hardwareMan =
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
hardwareMan.carInfo.fetchModel(exec) { model ->
|
||||
loadPrices(model)
|
||||
}
|
||||
} else {
|
||||
loadPrices(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = getDataAdapter() ?: return
|
||||
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
|
||||
)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
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)
|
||||
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)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDataAdapter(): String? = when (charger.dataSource) {
|
||||
"goingelectric" -> ChargepriceApi.DATA_SOURCE_GOINGELECTRIC
|
||||
"openchargemap" -> ChargepriceApi.DATA_SOURCE_OPENCHARGEMAP
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
@@ -48,7 +49,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
}
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
|
||||
private val imageSize = 128 // images should be 128dp according to docs
|
||||
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, oversize = 1.4f, height = imageSize)
|
||||
|
||||
init {
|
||||
referenceData.observe(this) {
|
||||
@@ -147,29 +151,49 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
navigateToCharger(charger)
|
||||
}
|
||||
.build())
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.open_in_app))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_CHARGER_ID, charger.id)
|
||||
.putExtra(EXTRA_LAT, charger.coordinates.lat)
|
||||
.putExtra(EXTRA_LON, charger.coordinates.lng)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
charger.chargepriceData?.country?.let { country ->
|
||||
if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
screenManager.push(ChargepriceScreen(carContext, charger))
|
||||
}
|
||||
.build())
|
||||
}
|
||||
}
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.open_in_app))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
|
||||
.putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
|
||||
.putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -26,15 +28,11 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
init {
|
||||
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
|
||||
emptyIcon = CarIcon.Builder(
|
||||
IconCompat.createWithBitmap(
|
||||
Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
)
|
||||
).build()
|
||||
emptyIcon = Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888
|
||||
).asCarIcon()
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -44,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)
|
||||
@@ -72,7 +73,9 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}.build())
|
||||
profiles.forEach {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(it.name)
|
||||
val name =
|
||||
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
|
||||
setTitle(name)
|
||||
if (it.id == filterStatus) {
|
||||
setImage(checkIcon)
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
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.*
|
||||
@@ -21,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
|
||||
@@ -32,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
|
||||
@@ -39,15 +47,19 @@ import kotlin.math.roundToInt
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen {
|
||||
private var updateCoroutine: Job? = null
|
||||
private var numUpdates = 0
|
||||
private val maxNumUpdates = 3
|
||||
|
||||
/* Updating map contents is disabled - if the user uses Chargeprice from the charger
|
||||
detail screen, this already means 4 steps, after which the app would crash.
|
||||
follow https://issuetracker.google.com/issues/176694222 for updates how to solve this. */
|
||||
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)
|
||||
@@ -55,20 +67,27 @@ 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()
|
||||
private val maxRows = 6
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
|
||||
} else 6
|
||||
|
||||
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()
|
||||
@@ -133,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
|
||||
}
|
||||
@@ -145,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(
|
||||
@@ -171,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
|
||||
)
|
||||
}
|
||||
@@ -214,18 +245,30 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
if (location.latitude == this.location?.latitude
|
||||
&& location.longitude == this.location?.longitude
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
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()
|
||||
}
|
||||
@@ -239,8 +282,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
/*CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()*/
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
@@ -263,7 +306,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
if (it.size < maxRows) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
@@ -301,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) {
|
||||
@@ -310,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)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
|
||||
class PermissionActivity : Activity() {
|
||||
companion object {
|
||||
const val EXTRA_RESULT_RECEIVER = "result_receiver";
|
||||
const val RESULT_GRANTED = "granted"
|
||||
}
|
||||
|
||||
private lateinit var resultReceiver: ResultReceiver
|
||||
private val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
private val requestCode = 1
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent != null) {
|
||||
resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)!!
|
||||
if (!hasPermissions(permissions)) {
|
||||
ActivityCompat.requestPermissions(this, permissions, requestCode)
|
||||
} else {
|
||||
onComplete(
|
||||
requestCode,
|
||||
permissions,
|
||||
intArrayOf(PackageManager.PERMISSION_GRANTED)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete(requestCode: Int, permissions: Array<String>?, grantResults: IntArray) {
|
||||
val bundle = Bundle()
|
||||
bundle.putBoolean(
|
||||
RESULT_GRANTED,
|
||||
grantResults.all { it == PackageManager.PERMISSION_GRANTED })
|
||||
resultReceiver.send(requestCode, bundle)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun hasPermissions(permissions: Array<String>): Boolean {
|
||||
var result = true
|
||||
for (permission in permissions) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
onComplete(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
/**
|
||||
* Screen to grant location permission
|
||||
* Screen to grant permission
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
class PermissionScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val message: Int,
|
||||
val permissions: List<String>
|
||||
) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
return MessageTemplate.Builder(carContext.getString(message))
|
||||
.setTitle(carContext.getString(R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
@@ -23,32 +23,7 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
|
||||
.setTitle(carContext.getString(R.string.grant_on_phone))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, PermissionActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(
|
||||
PermissionActivity.EXTRA_RESULT_RECEIVER,
|
||||
object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(
|
||||
resultCode: Int,
|
||||
resultData: Bundle?
|
||||
) {
|
||||
if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
|
||||
session.bindLocationService()
|
||||
screenManager.push(
|
||||
WelcomeScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
requestPermissions()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
@@ -62,4 +37,14 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
carContext.requestPermissions(permissions) { granted, rejected ->
|
||||
if (granted.containsAll(permissions)) {
|
||||
screenManager.pop()
|
||||
} else {
|
||||
requestPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.car.app.CarContext
|
||||
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 }
|
||||
@@ -17,4 +28,122 @@ fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
} else {
|
||||
CarColor.BLUE
|
||||
}
|
||||
}
|
||||
|
||||
val CarContext.constraintManager
|
||||
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as 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 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
|
||||
else -> CarUnit.KILOMETERS_PER_HOUR
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCarUnitDistance(value: Float?, unit: Int?): String {
|
||||
if (value == null) return ""
|
||||
return when (unit ?: getDefaultDistanceUnit()) {
|
||||
// distance units: base unit is meters
|
||||
CarUnit.METER -> "%.0f m".format(value)
|
||||
CarUnit.KILOMETER -> "%.1f km".format(value / 1000)
|
||||
CarUnit.MILLIMETER -> "%.0f mm".format(value * 1000) // whoever uses that...
|
||||
CarUnit.MILE -> "%.1f mi".format(value / 1000 / kmPerMile)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
|
||||
if (value == null) return ""
|
||||
return when (unit ?: getDefaultSpeedUnit()) {
|
||||
// speed units: base unit is meters per second
|
||||
CarUnit.METERS_PER_SEC -> "%.0f m/s".format(value)
|
||||
CarUnit.KILOMETERS_PER_HOUR -> "%.0f km/h".format(value * 3.6)
|
||||
CarUnit.MILES_PER_HOUR -> "%.0f mph".format(value * 3.6 / kmPerMile)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
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(".")
|
||||
}
|
||||
|
||||
fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
if (ctx.carAppApiLevel < CarAppApiLevels.LEVEL_3) return false
|
||||
ctx.hostInfo?.let { hostInfo ->
|
||||
if (hostInfo.packageName == "com.google.android.projection.gearhead") {
|
||||
val version = getAndroidAutoVersion(ctx)
|
||||
// Android Auto 6.7 is required. 6.6 reports supporting API Level 3,
|
||||
// but crashes when using it. See: https://issuetracker.google.com/issues/199509584
|
||||
if (version[0] < "6" || version[0] == "6" && version[1] < "7") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
215
app/src/google/java/net/vonforst/evmap/auto/VehicleDataScreen.kt
Normal file
@@ -0,0 +1,215 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.hardware.info.Speed
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.Gauge
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private var model: Model? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var speed: Speed? = null
|
||||
private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
|
||||
private val maxSpeed = 160f / 3.6f // m/s, speed gauge will show max if speed is higher
|
||||
|
||||
private val permissions = listOf(
|
||||
"com.google.android.gms.permission.CAR_FUEL",
|
||||
"com.google.android.gms.permission.CAR_SPEED"
|
||||
)
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (!permissionsGranted()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
screenManager.pushForResult(
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_vehicle_data_permission_needed,
|
||||
permissions
|
||||
)
|
||||
) {
|
||||
setupListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val energyLevel = energyLevel
|
||||
val model = model
|
||||
val speed = speed
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(
|
||||
if (model != null && model.manufacturer.value != null && model.name.value != null) {
|
||||
"${model.manufacturer.value} ${model.name.value}"
|
||||
} else {
|
||||
carContext.getString(R.string.auto_vehicle_data)
|
||||
}
|
||||
)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (!permissionsGranted()) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setSingleList(
|
||||
ItemList.Builder().apply {
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_charging_level))
|
||||
if (energyLevel == null) {
|
||||
setLoading(true)
|
||||
} else if (energyLevel.batteryPercent.value != null && energyLevel.fuelPercent.value != null) {
|
||||
// both battery and fuel (Plug-in hybrid)
|
||||
setText(
|
||||
"\uD83D\uDD0C %.0f %% ⛽ %.0f %%".format(
|
||||
energyLevel.batteryPercent.value,
|
||||
energyLevel.fuelPercent.value
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
gauge.draw(
|
||||
energyLevel.batteryPercent.value,
|
||||
energyLevel.fuelPercent.value
|
||||
).asCarIcon()
|
||||
)
|
||||
} else if (energyLevel.batteryPercent.value != null) {
|
||||
// BEV
|
||||
setText("%.0f %%".format(energyLevel.batteryPercent.value))
|
||||
setImage(gauge.draw(energyLevel.batteryPercent.value).asCarIcon())
|
||||
} else if (energyLevel.fuelPercent.value != null) {
|
||||
// ICE
|
||||
setText("⛽ %.0f %%".format(energyLevel.fuelPercent.value))
|
||||
setImage(gauge.draw(energyLevel.fuelPercent.value).asCarIcon())
|
||||
} else {
|
||||
setText(carContext.getString(R.string.auto_no_data))
|
||||
setImage(gauge.draw(0f).asCarIcon())
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_range))
|
||||
if (energyLevel == null) {
|
||||
setLoading(true)
|
||||
} else if (energyLevel.rangeRemainingMeters.value != null) {
|
||||
setText(
|
||||
formatCarUnitDistance(
|
||||
energyLevel.rangeRemainingMeters.value,
|
||||
energyLevel.distanceDisplayUnit.value
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_car
|
||||
)
|
||||
).build()
|
||||
)
|
||||
} else {
|
||||
setText(carContext.getString(R.string.auto_no_data))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_car
|
||||
)
|
||||
).build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_speed))
|
||||
if (speed == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val rawSpeed = speed.rawSpeedMetersPerSecond.value
|
||||
val displaySpeed = speed.displaySpeedMetersPerSecond.value
|
||||
if (rawSpeed != null) {
|
||||
setText(
|
||||
formatCarUnitSpeed(
|
||||
rawSpeed,
|
||||
speed.speedDisplayUnit.value
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
gauge.draw(min(rawSpeed / maxSpeed * 100, 100f)).asCarIcon()
|
||||
)
|
||||
} else if (displaySpeed != null) {
|
||||
setText(
|
||||
formatCarUnitSpeed(
|
||||
speed.displaySpeedMetersPerSecond.value,
|
||||
speed.speedDisplayUnit.value
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
gauge.draw(min(displaySpeed / maxSpeed * 100, 100f))
|
||||
.asCarIcon()
|
||||
)
|
||||
} else {
|
||||
setText(carContext.getString(R.string.auto_no_data))
|
||||
setImage(gauge.draw(0f).asCarIcon())
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
this.energyLevel = energyLevel
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun onSpeedUpdated(speed: Speed) {
|
||||
this.speed = speed
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun setupListeners() {
|
||||
if (!permissionsGranted()) return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.addSpeedListener(exec, ::onSpeedUpdated)
|
||||
|
||||
hardwareMan.carInfo.fetchModel(exec) {
|
||||
this.model = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.removeSpeedListener(::onSpeedUpdated)
|
||||
}
|
||||
|
||||
private fun permissionsGranted(): Boolean =
|
||||
permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
@@ -10,60 +13,108 @@ import net.vonforst.evmap.R
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (!session.locationPermissionGranted()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
screenManager.pushForResult(
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
) {
|
||||
session.bindLocationService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.app_name))
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
}
|
||||
setItemList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_address
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = false))
|
||||
}
|
||||
.build())
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_favorites))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
if (!session.locationPermissionGranted()) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
}
|
||||
setItemList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_address
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(
|
||||
MapScreen(
|
||||
carContext,
|
||||
session,
|
||||
favorites = false
|
||||
)
|
||||
)
|
||||
}
|
||||
.build())
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_favorites))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_vehicle_data))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(carContext, R.drawable.ic_car)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
session.mapScreen = null
|
||||
screenManager.push(VehicleDataScreen(carContext))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
}
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
}
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
if (location.latitude == this.location?.latitude
|
||||
&& location.longitude == this.location?.longitude
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
74
app/src/google/java/net/vonforst/evmap/ui/Gauge.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import net.vonforst.evmap.R
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class Gauge(val size: Int, ctx: Context) {
|
||||
val arcPaint = Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = size * 0.15f
|
||||
}
|
||||
val gaugePaint = Paint()
|
||||
val activeColor = ContextCompat.getColor(ctx, R.color.gauge_active)
|
||||
val middleColor = ContextCompat.getColor(ctx, R.color.gauge_middle)
|
||||
val inactiveColor = ContextCompat.getColor(ctx, R.color.gauge_inactive)
|
||||
|
||||
fun draw(valuePercent: Float?, secondValuePercent: Float? = null): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
|
||||
val angle = valuePercent?.let { 180f * it / 100 } ?: 0f
|
||||
val secondAngle = secondValuePercent?.let { 180f * it / 100 }
|
||||
|
||||
drawArc(angle, secondAngle, canvas)
|
||||
if (secondAngle != null) drawGauge(secondAngle, inactiveColor, canvas)
|
||||
drawGauge(angle, Color.WHITE, canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun drawGauge(angle: Float, @ColorInt color: Int, canvas: Canvas) {
|
||||
gaugePaint.color = color
|
||||
canvas.save()
|
||||
canvas.rotate(angle - 90, size / 2f, 3 * size / 4f)
|
||||
canvas.drawCircle(size / 2f, 3 * size / 4f, size * 0.1F, gaugePaint)
|
||||
canvas.drawRect(size * 0.48f, 3 * size / 4f, size * 0.53f, size * 0.325f, gaugePaint)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
private fun drawArc(angle: Float, secondAngle: Float?, canvas: Canvas) {
|
||||
val (angle1, angle2) = if (secondAngle != null) {
|
||||
min(angle, secondAngle) to max(angle, secondAngle)
|
||||
} else {
|
||||
angle to null
|
||||
}
|
||||
|
||||
arcPaint.color = activeColor
|
||||
val arcBounds = RectF(
|
||||
arcPaint.strokeWidth / 2,
|
||||
size / 4f + arcPaint.strokeWidth / 2,
|
||||
size - arcPaint.strokeWidth / 2,
|
||||
5 * size / 4f - arcPaint.strokeWidth / 2
|
||||
)
|
||||
|
||||
canvas.drawArc(arcBounds, 180f, angle1, false, arcPaint)
|
||||
if (angle2 != null) {
|
||||
arcPaint.color = middleColor
|
||||
canvas.drawArc(arcBounds, 180f + angle1, angle2 - angle1, false, arcPaint)
|
||||
}
|
||||
arcPaint.color = inactiveColor
|
||||
canvas.drawArc(
|
||||
arcBounds,
|
||||
180f + (angle2 ?: angle1),
|
||||
180f - (angle2 ?: angle1),
|
||||
false,
|
||||
arcPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
10
app/src/google/res/drawable/ic_car.xml
Normal 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="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11c-0.66,0 -1.21,0.42 -1.42,1.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8l-2.08,-5.99zM6.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,13 6.5,13s1.5,0.67 1.5,1.5S7.33,16 6.5,16zM17.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM5,11l1.5,-4.5h11L19,11L5,11z" />
|
||||
</vector>
|
||||
@@ -15,12 +15,22 @@
|
||||
<string name="open_in_app">In App öffnen</string>
|
||||
<string name="opened_on_phone">Auf dem Telefon geöffnet</string>
|
||||
<string name="auto_location_permission_needed">Um EVMap auf Android Auto zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Für diese Funktion benötigt EVMap Zugriff auf Daten deines Fahrzeugs.</string>
|
||||
<string name="grant_on_phone">Auf Telefon zulassen</string>
|
||||
<string name="auto_chargers_closeby">In der Nähe</string>
|
||||
<string name="auto_favorites">Favoriten</string>
|
||||
<string name="auto_fault_report_date">⚠️ Störungsmeldung (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.</string>
|
||||
<string name="auto_prices">Preise</string>
|
||||
<string name="auto_vehicle_data">Fahrzeugdaten</string>
|
||||
<string name="auto_charging_level">Ladezustand</string>
|
||||
<string name="auto_no_data">Nicht verfügbar</string>
|
||||
<string name="auto_range">Reichweite</string>
|
||||
<string name="auto_speed">Geschwindigkeit</string>
|
||||
<string name="welcome_android_auto">Android Auto-Unterstützung</string>
|
||||
<string name="welcome_android_auto_detail">Auf unterstützen Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.</string>
|
||||
<string name="sounds_cool">klingt cool</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap konnte das Fahrzeugmodell nicht erkennen.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%s %s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%s %s).</string>
|
||||
</resources>
|
||||
7
app/src/google/res/values/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
@@ -25,12 +25,22 @@
|
||||
<string name="open_in_app">Open in app</string>
|
||||
<string name="opened_on_phone">Opened on phone</string>
|
||||
<string name="auto_location_permission_needed">To run EVMap on Android Auto, you need to grant access to your location.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">For this feature, EVMap needs access to your vehicle data.</string>
|
||||
<string name="grant_on_phone">Grant on phone</string>
|
||||
<string name="auto_chargers_closeby">Nearby chargers</string>
|
||||
<string name="auto_favorites">Favorites</string>
|
||||
<string name="auto_fault_report_date">⚠️ Fault report (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Further updates not possible. Please go back and restart.</string>
|
||||
<string name="auto_prices">Pricing</string>
|
||||
<string name="auto_vehicle_data">Vehicle data</string>
|
||||
<string name="auto_charging_level">Charging level</string>
|
||||
<string name="auto_no_data">Unavailable</string>
|
||||
<string name="auto_range">Range</string>
|
||||
<string name="auto_speed">Speed</string>
|
||||
<string name="welcome_android_auto">Android Auto support</string>
|
||||
<string name="welcome_android_auto_detail">You can also use EVMap from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
|
||||
<string name="sounds_cool">sounds cool</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap could not determine your vehicle model.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">None of the vehicles selected in the app matches this vehicle (%s %s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%s %s).</string>
|
||||
</resources>
|
||||
@@ -3,6 +3,7 @@
|
||||
package="net.vonforst.evmap">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<queries>
|
||||
@@ -31,8 +32,9 @@
|
||||
|
||||
<activity
|
||||
android:name=".MapsActivity"
|
||||
android:label="@string/title_activity_maps"
|
||||
android:theme="@style/AppTheme.LaunchScreen">
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.LaunchScreen"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -256,6 +258,16 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Override services of the com.mapzen.android.lost library with exported:false
|
||||
until https://github.com/lostzen/lost/pull/270 is merged -->
|
||||
<service
|
||||
android:name="com.mapzen.android.lost.internal.GeofencingIntentService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.mapzen.lost.action.ACTION_GEOFENCING_SERVICE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -4,22 +4,30 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
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
|
||||
@@ -31,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
|
||||
}
|
||||
@@ -51,9 +60,8 @@ class MapsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// set theme to AppTheme to end launch screen
|
||||
setTheme(R.style.AppTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
val splashScreen = installSplashScreen()
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
@@ -82,13 +90,28 @@ class MapsActivity : AppCompatActivity() {
|
||||
|
||||
checkPlayServices(this)
|
||||
|
||||
|
||||
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
|
||||
navGraph.startDestination = R.id.onboarding
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// wait for splash screen animation to finish on first start
|
||||
splashScreen.setKeepVisibleCondition(object : SplashScreen.KeepOnScreenCondition {
|
||||
var startTime: Long? = null
|
||||
|
||||
override fun shouldKeepOnScreen(): Boolean {
|
||||
val st = startTime
|
||||
if (st == null) {
|
||||
startTime = SystemClock.uptimeMillis()
|
||||
return true
|
||||
} else {
|
||||
return (SystemClock.uptimeMillis() - st) < 1000
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
navGraph.setStartDestination(R.id.onboarding)
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else {
|
||||
navGraph.startDestination = R.id.map
|
||||
navGraph.setStartDestination(R.id.map)
|
||||
navController.graph = navGraph
|
||||
}
|
||||
|
||||
@@ -103,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()
|
||||
}
|
||||
@@ -120,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()
|
||||
}
|
||||
@@ -128,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()
|
||||
@@ -195,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -100,7 +100,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
var i = 0
|
||||
gePowers.map { gePower ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type == type && it.power == gePower }!!
|
||||
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
val ids = allIds.subList(i, i + chargepoint.count).toSet()
|
||||
i += chargepoint.count
|
||||
chargepoint to ids
|
||||
|
||||
@@ -15,6 +15,7 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import java.util.*
|
||||
|
||||
interface ChargepriceApi {
|
||||
@POST("charge_prices")
|
||||
@@ -33,6 +34,9 @@ interface ChargepriceApi {
|
||||
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||
val supportedLanguages = setOf("de", "en", "fr", "nl")
|
||||
|
||||
val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||
val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||
|
||||
private val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
|
||||
.add(ChargepriceRequest::class.java)
|
||||
.add(ChargepriceTariff::class.java)
|
||||
@@ -75,6 +79,16 @@ interface ChargepriceApi {
|
||||
return retrofit.create(ChargepriceApi::class.java)
|
||||
}
|
||||
|
||||
|
||||
fun getChargepriceLanguage(): String {
|
||||
val locale = Locale.getDefault().language
|
||||
return if (supportedLanguages.contains(locale)) {
|
||||
locale
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
|
||||
// list of countries updated 2021/08/24
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
@@ -148,6 +149,26 @@ class ChargepriceCar : Resource(), Equatable {
|
||||
result = 31 * result + manufacturer.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
val compatibleEvmapConnectors: List<String>
|
||||
get() = dcChargePorts.map {
|
||||
plugMapping[it]
|
||||
}.filterNotNull().plus(acConnectors)
|
||||
}
|
||||
|
||||
@JsonApi(type = "brand")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
|
||||
interface AutocompleteProvider {
|
||||
val id: String
|
||||
|
||||
fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace>
|
||||
suspend fun getDetails(id: String): PlaceWithBounds
|
||||
|
||||
@@ -20,7 +22,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 +169,8 @@ enum class AutocompletePlaceType {
|
||||
TRAVEL_AGENCY,
|
||||
UNIVERSITY,
|
||||
VETERINARY_CARE,
|
||||
ZOO;
|
||||
ZOO,
|
||||
RECENT;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrNull(value: String): AutocompletePlaceType? {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
@@ -216,28 +213,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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Canvas
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
@@ -9,7 +7,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -30,12 +27,13 @@ import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
|
||||
import net.vonforst.evmap.databinding.ItemFavoriteBinding
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
private lateinit var binding: FragmentFavoritesBinding
|
||||
private lateinit var locationClient: LostApiClient
|
||||
private var locationClient: LostApiClient? = null
|
||||
private var toDelete: ChargeLocation? = null
|
||||
private var deleteSnackbar: Snackbar? = null
|
||||
private lateinit var adapter: FavoritesAdapter
|
||||
@@ -49,11 +47,17 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this).build()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_favorites, container, false
|
||||
@@ -61,9 +65,6 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this).build()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -76,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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -92,17 +96,13 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
}
|
||||
createTouchHelper().attachToRecyclerView(binding.favsList)
|
||||
|
||||
locationClient.connect()
|
||||
locationClient!!.connect()
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val context = this.context ?: return
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (context.checkAnyLocationPermission()) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient!!)
|
||||
if (location != null) {
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
@@ -115,8 +115,8 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (locationClient.isConnected) {
|
||||
locationClient.disconnect()
|
||||
locationClient?.let {
|
||||
if (it.isConnected) it.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -32,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
|
||||
@@ -71,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
|
||||
@@ -81,16 +84,13 @@ import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
import net.vonforst.evmap.ui.MarkerAnimator
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.boundingBox
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
import net.vonforst.evmap.utils.checkFineLocationPermission
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.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
|
||||
@@ -239,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) {
|
||||
@@ -262,10 +264,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
|
||||
vm.reloadPrefs()
|
||||
if (requestingLocationUpdates && ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED && locationClient.isConnected
|
||||
if (requestingLocationUpdates && requireContext().checkAnyLocationPermission()
|
||||
&& locationClient.isConnected
|
||||
) {
|
||||
requestLocationUpdates()
|
||||
}
|
||||
@@ -273,16 +273,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.fabLocate.setOnClickListener {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissions(
|
||||
arrayOf(ACCESS_FINE_LOCATION),
|
||||
if (!requireContext().checkFineLocationPermission()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION),
|
||||
REQUEST_LOCATION_PERMISSION
|
||||
)
|
||||
} else {
|
||||
}
|
||||
if (requireContext().checkAnyLocationPermission()) {
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
@@ -308,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 {
|
||||
@@ -369,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) {
|
||||
@@ -389,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
|
||||
}
|
||||
@@ -444,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() {
|
||||
@@ -477,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()
|
||||
@@ -575,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()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -589,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)
|
||||
@@ -602,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()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -804,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
|
||||
|
||||
@@ -816,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(
|
||||
@@ -834,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,
|
||||
@@ -883,11 +896,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
if (context?.checkAnyLocationPermission() ?: false) {
|
||||
enableLocation(!positionSet, false)
|
||||
positionSet = true
|
||||
}
|
||||
@@ -903,7 +912,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
|
||||
private fun enableLocation(moveTo: Boolean, animate: Boolean) {
|
||||
val map = this.map ?: return
|
||||
map.setMyLocationEnabled(true)
|
||||
@@ -917,7 +926,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
|
||||
private fun moveToLastLocation(map: AnyMap, animate: Boolean) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (location != null) {
|
||||
@@ -949,7 +958,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()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -967,7 +977,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)
|
||||
}
|
||||
@@ -982,6 +993,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))
|
||||
@@ -992,12 +1004,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
|
||||
}
|
||||
}
|
||||
@@ -1027,7 +1040,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_LOCATION_PERMISSION -> {
|
||||
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
if ((grantResults.isNotEmpty() && grantResults.any { it == PackageManager.PERMISSION_GRANTED })) {
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
@@ -1089,6 +1102,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,
|
||||
@@ -1105,11 +1123,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) {
|
||||
@@ -1121,6 +1140,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]
|
||||
@@ -1159,52 +1182,11 @@ 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
|
||||
if (vm.myLocationEnabled.value == true) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
if (context.checkAnyLocationPermission()) {
|
||||
moveToLastLocation(map, false)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -93,12 +95,18 @@ class WelcomeFragment : OnboardingPageFragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.animationView.playAnimation()
|
||||
val drawable = (binding.animationView as ImageView).drawable
|
||||
if (drawable is AnimatedVectorDrawable) {
|
||||
drawable.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.animationView.progress = 0f
|
||||
val drawable = (binding.animationView as ImageView).drawable
|
||||
if (drawable is AnimatedVectorDrawable) {
|
||||
drawable.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
@@ -23,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
|
||||
@@ -50,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) {
|
||||
@@ -256,6 +259,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
|
||||
private val MIGRATION_13 = object : Migration(12, 13) {
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
@@ -301,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`))");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlin.math.*
|
||||
@@ -75,3 +79,17 @@ fun boundingBox(pos: LatLng, sizeMeters: Double): LatLngBounds {
|
||||
pos.plusMeters(sizeMeters, sizeMeters)
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.checkAnyLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
fun Context.checkFineLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
@@ -49,27 +49,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
MutableLiveData<ChargepriceCar>()
|
||||
}
|
||||
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
|
||||
MediatorLiveData<List<String>>().apply {
|
||||
addSource(vehicle) {
|
||||
value = it?.dcChargePorts?.map {
|
||||
plugMapping[it]
|
||||
}?.filterNotNull()?.plus(acConnectors)
|
||||
value = it?.compatibleEvmapConnectors
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,12 +184,16 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
} else if (cpMeta.status == Status.LOADING) {
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
value =
|
||||
Resource.success(cpMeta.data!!.chargePoints.filter {
|
||||
it.plug == getChargepricePlugType(
|
||||
chargepoint
|
||||
) && it.power == chargepoint.power
|
||||
}[0])
|
||||
val result = cpMeta.data!!.chargePoints.filter {
|
||||
it.plug == getChargepricePlugType(
|
||||
chargepoint
|
||||
) && it.power == chargepoint.power
|
||||
}.elementAtOrNull(0)
|
||||
value = if (result != null) {
|
||||
Resource.success(result)
|
||||
} else {
|
||||
Resource.error("matching chargepoint not found", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +228,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency
|
||||
)
|
||||
}, getChargepriceLanguage())
|
||||
}, ChargepriceApi.getChargepriceLanguage())
|
||||
val meta =
|
||||
result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta
|
||||
chargePrices.value = Resource.success(result)
|
||||
@@ -268,13 +255,4 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChargepriceLanguage(): String {
|
||||
val locale = Locale.getDefault().language
|
||||
return if (ChargepriceApi.supportedLanguages.contains(locale)) {
|
||||
locale
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
|
||||
@@ -296,20 +297,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 +346,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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="144.3dp"
|
||||
android:height="270.5dp"
|
||||
android:viewportWidth="144.3"
|
||||
android:viewportHeight="270.5">
|
||||
<path
|
||||
android:pathData="M33.9,100l-2.5,-21.7l-3.8,0.4l2.5,21.7L33.9,100zM47.4,98.5l-2.5,-21.7l-3.8,0.4l2.5,21.7L47.4,98.5z"
|
||||
android:fillColor="#FFB300" />
|
||||
<path
|
||||
android:pathData="M54.5,128c-1.2,1.4 -2.1,2.4 -2.2,2.5c-3.4,2.7 -6.1,3.5 -8.4,2.5c-3.9,-2 -3.7,-9.3 -3.5,-10.1l2.7,0.1c-0.1,2.1 0.3,6.5 2.1,7.5c1,0.5 2.9,-0.1 5.2,-2.1l0,0c0,0 7.6,-7.6 6,-13.6c-1.8,-7.2 6.5,-17.5 9.3,-21.1l0.4,-0.4l2.2,1.7l-0.4,0.5c-8.5,10.5 -9.4,15.8 -8.8,18.6C60.5,119.4 57,125 54.5,128z"
|
||||
android:fillColor="#90A4AE" />
|
||||
<path
|
||||
android:pathData="M25.6,99.8l1,8.9l8.2,5.5L46,113l6.8,-7.2l-1,-8.9L25.6,99.8z"
|
||||
android:fillColor="#90A4AE" />
|
||||
<path
|
||||
android:pathData="M45.8,113l-11.1,1.2l2.4,9.8l8.8,-1V113L45.8,113zM53.8,89.4l0.9,8.1l-31.9,3.7l-0.9,-8.1L53.8,89.4z"
|
||||
android:fillColor="#546E7A" />
|
||||
<path
|
||||
android:pathData="M78.8,0C55.9,0 37.3,18.6 37.3,41.5c0,31.3 34.9,47.6 39.1,92.2c0.1,1.3 1.2,2.2 2.5,2.2s2.4,-0.9 2.5,-2.2c4.2,-44.6 39.1,-60.9 39.1,-92.2C120.3,18.4 101.7,0 78.8,0z"
|
||||
android:fillColor="#00E676" />
|
||||
<path
|
||||
android:pathData="M78.8,0.9c22.8,0 41.2,18.3 41.5,40.9c0,-0.1 0,-0.3 0,-0.4C120.3,18.6 101.7,0 78.8,0S37.3,18.4 37.3,41.5c0,0.1 0,0.3 0,0.4C37.6,19.2 56,0.9 78.8,0.9L78.8,0.9z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.2" />
|
||||
<path
|
||||
android:pathData="M81.3,132.6c-0.1,1.3 -1.2,2.2 -2.5,2.2c-1.3,0 -2.4,-0.9 -2.5,-2.2c-4.1,-44.5 -38.7,-60.8 -39,-91.7c0,0.3 0,0.4 0,0.7c0,31.3 34.9,47.6 39.1,92.2c0.1,1.3 1.2,2.2 2.5,2.2c1.3,0 2.4,-0.9 2.5,-2.2c4.2,-44.6 39.1,-60.9 39.1,-92.2c0,-0.3 0,-0.4 0,-0.7C120,71.8 85.3,88.1 81.3,132.6L81.3,132.6z"
|
||||
android:fillColor="#3E2723"
|
||||
android:fillAlpha="0.2" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M69.3,21.2v25.1h6.8v20.5l16,-27.5h-9.2L92,21.1C92.1,21.2 69.3,21.2 69.3,21.2z"
|
||||
android:strokeAlpha="0.45"
|
||||
android:fillAlpha="0.45" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M19.2,244.2H2.8v14.1h18.8v2.4H0v-34.1h21.5v2.4H2.8v12.8h16.4V244.2z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M37.2,254.9l0.7,2.3h0.1l0.7,-2.3L49,226.6h3l-12.7,34.1h-2.6l-12.7,-34.1h3L37.2,254.9z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M60.9,226.6l12.5,30h0.1l12.6,-30h3.7v34.1h-2.8v-15.1l0.2,-14.9l-0.1,0l-12.7,30h-1.9l-12.7,-29.9l-0.1,0l0.3,14.8v15.1h-2.8v-34.1H60.9z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M114.1,260.7c-0.2,-0.9 -0.3,-1.6 -0.4,-2.2s-0.1,-1.3 -0.1,-1.9c-0.9,1.3 -2.2,2.4 -3.8,3.3s-3.3,1.3 -5.3,1.3c-2.5,0 -4.4,-0.7 -5.8,-2s-2.1,-3.1 -2.1,-5.3c0,-2.3 1,-4.2 3,-5.6s4.8,-2.1 8.2,-2.1h5.6v-3.1c0,-1.8 -0.6,-3.2 -1.7,-4.3s-2.8,-1.5 -4.9,-1.5c-2,0 -3.6,0.5 -4.9,1.5s-1.9,2.2 -1.9,3.6l-2.6,0l0,-0.1c-0.1,-1.9 0.8,-3.6 2.6,-5.1s4.1,-2.2 6.9,-2.2c2.8,0 5,0.7 6.8,2.1s2.6,3.5 2.6,6.1v12.5c0,0.9 0.1,1.8 0.2,2.6s0.3,1.7 0.5,2.5H114.1zM104.9,258.7c2,0 3.8,-0.5 5.3,-1.4s2.7,-2.2 3.4,-3.6v-5.3H108c-2.5,0 -4.6,0.5 -6.1,1.6s-2.3,2.4 -2.3,4c0,1.4 0.5,2.5 1.4,3.4S103.3,258.7 104.9,258.7z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M144.3,248.7c0,3.8 -0.9,6.8 -2.6,9.1s-4.1,3.4 -7.1,3.4c-1.8,0 -3.3,-0.3 -4.7,-1s-2.4,-1.6 -3.3,-2.9v13.1h-2.8v-35.1h2.4l0.4,3.9c0.8,-1.4 1.9,-2.5 3.3,-3.3s2.9,-1.1 4.7,-1.1c3,0 5.4,1.2 7.1,3.6s2.6,5.7 2.6,9.7V248.7zM141.5,248.2c0,-3.2 -0.6,-5.8 -1.9,-7.9c-1.3,-2 -3.2,-3 -5.6,-3c-1.9,0 -3.4,0.4 -4.6,1.3c-1.2,0.9 -2.1,2.1 -2.7,3.5v12.2c0.6,1.4 1.6,2.5 2.8,3.3s2.7,1.2 4.5,1.2c2.5,0 4.3,-0.9 5.6,-2.8c1.3,-1.8 1.9,-4.3 1.9,-7.3V248.2z" />
|
||||
</vector>
|
||||
@@ -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>
|
||||
|
||||
10
app/src/main/res/drawable/ic_history.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_map_marker_fav.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/ic_settings_data_source.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_settings_ui.xml
Normal 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>
|
||||
217
app/src/main/res/drawable/intro_anim.xml
Normal file
@@ -0,0 +1,217 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
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="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_2_G"
|
||||
android:pivotX="53.625"
|
||||
android:pivotY="43.025"
|
||||
android:rotation="73"
|
||||
android:translateX="-37.85"
|
||||
android:translateY="6.550000000000004">
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffb300"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M9.45 18.05 C9.45,18.05 7.55,1.45 7.55,1.45 C7.55,1.45 4.65,1.75 4.65,1.75 C4.65,1.75 6.55,18.35 6.55,18.35 C6.55,18.35 9.45,18.05 9.45,18.05c M19.75 16.85 C19.75,16.85 17.85,0.25 17.85,0.25 C17.85,0.25 14.95,0.55 14.95,0.55 C14.95,0.55 16.85,17.15 16.85,17.15 C16.85,17.15 19.75,16.85 19.75,16.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_1_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M25.15 39.45 C24.25,40.55 23.55,41.25 23.45,41.35 C20.85,43.45 18.75,44.05 17.05,43.25 C14.05,41.75 14.25,36.15 14.35,35.55 C14.35,35.55 16.45,35.65 16.45,35.65 C16.35,37.25 16.65,40.65 18.05,41.35 C18.85,41.75 20.25,41.25 22.05,39.75 C22.05,39.75 27.85,33.95 26.65,29.35 C25.25,23.85 31.65,15.95 33.75,13.25 C33.75,13.25 34.05,12.95 34.05,12.95 C34.05,12.95 35.75,14.25 35.75,14.25 C35.75,14.25 35.45,14.65 35.45,14.65 C28.95,22.65 28.25,26.75 28.75,28.85 C29.75,32.85 27.05,37.15 25.15,39.45c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_2_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M3.05 17.85 C3.05,17.85 3.85,24.65 3.85,24.65 C3.85,24.65 10.15,28.85 10.15,28.85 C10.15,28.85 18.65,27.95 18.65,27.95 C18.65,27.95 23.85,22.45 23.85,22.45 C23.85,22.45 23.05,15.65 23.05,15.65 C23.05,15.65 3.05,17.85 3.05,17.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_3_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#546e7a"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M18.55 27.95 C18.55,27.95 10.05,28.85 10.05,28.85 C10.05,28.85 11.85,36.35 11.85,36.35 C11.85,36.35 18.55,35.55 18.55,35.55 C18.55,35.55 18.55,27.95 18.55,27.95c M24.65 9.95 C24.65,9.95 25.35,16.15 25.35,16.15 C25.35,16.15 0.95,18.95 0.95,18.95 C0.95,18.95 0.25,12.75 0.25,12.75 C0.25,12.75 24.65,9.95 24.65,9.95c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_1_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_1_G"
|
||||
android:translateX="-26.049"
|
||||
android:translateY="-52.150000000000006">
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00e676"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.25 C14.45,0.25 0.25,14.45 0.25,31.95 C0.25,55.85 26.95,68.35 30.15,102.45 C30.25,103.45 31.05,104.15 32.05,104.15 C33.05,104.15 33.85,103.45 33.95,102.45 C37.15,68.35 63.85,55.85 63.85,31.95 C63.65,14.35 49.45,0.25 31.95,0.25c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.95 C49.35,0.95 63.45,14.95 63.65,32.25 C63.65,32.25 63.65,31.95 63.65,31.95 C63.65,14.45 49.45,0.25 31.95,0.25 C14.45,0.25 0.25,14.35 0.25,31.95 C0.25,31.95 0.25,32.25 0.25,32.25 C0.45,14.95 14.55,0.95 31.95,0.95c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#3e2723"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M33.85 101.65 C33.75,102.65 32.95,103.35 31.95,103.35 C30.95,103.35 30.15,102.65 30.05,101.65 C26.95,67.65 0.45,55.15 0.25,31.55 C0.25,31.55 0.25,32.05 0.25,32.05 C0.25,55.95 26.95,68.45 30.15,102.55 C30.25,103.55 31.05,104.25 32.05,104.25 C33.05,104.25 33.85,103.55 33.95,102.55 C37.15,68.45 63.85,55.95 63.85,32.05 C63.85,32.05 63.85,31.55 63.85,31.55 C63.45,55.15 36.95,67.65 33.85,101.65c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_3_P_0"
|
||||
android:fillAlpha="0.45"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M24.65 16.45 C24.65,16.45 24.65,35.65 24.65,35.65 C24.65,35.65 29.85,35.65 29.85,35.65 C29.85,35.65 29.85,51.35 29.85,51.35 C29.85,51.35 42.05,30.35 42.05,30.35 C42.05,30.35 35.05,30.35 35.05,30.35 C35.05,30.35 42.05,16.35 42.05,16.35 C42.15,16.45 24.65,16.45 24.65,16.45c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_0_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_0_G"
|
||||
android:translateX="-1.3999999999999995"
|
||||
android:translateY="-35.8">
|
||||
<group android:name="_R_G_L_0_C_0_G">
|
||||
<clip-path
|
||||
android:name="_R_G_L_0_C_0"
|
||||
android:pathData=" M23.73 37.15 C23.73,37.15 21.71,37.15 21.71,37.15 C21.71,37.15 13.39,37.15 13.39,37.15 C13.39,37.15 -2.01,37.15 -2.01,37.15 C-2.01,37.15 -6.82,37.15 -6.82,37.15 C-6.82,37.15 -8.27,37.15 -8.27,37.15 C-8.27,37.15 -8.27,40.03 -8.27,40.03 C-8.27,40.03 23.73,40.03 23.73,40.03 C23.73,40.03 23.73,37.15 23.73,37.15c " />
|
||||
<group android:name="_R_G_L_0_C_0_G_G">
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,19.3 0,19.3 C0,19.3 5.2,19.3 5.2,19.3 C5.2,19.3 5.2,35 5.2,35 C5.2,35 17.4,14 17.4,14 C17.4,14 10.4,14 10.4,14 C10.4,14 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,0.69 0,0.69 C0,0.69 0,19.3 0,19.3 C0,19.3 0.55,19.3 0.55,19.3 C0.55,19.3 0.55,0.69 0.55,0.69 C0.55,0.69 17.05,0.69 17.05,0.69 C17.05,0.69 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M5.82 33.92 C5.82,33.92 5.2,35 5.2,35 C5.2,35 5.2,19.3 5.2,19.3 C5.2,19.3 5.82,19.3 5.82,19.3 C5.82,19.3 5.82,33.92 5.82,33.92c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_3_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M17.08 14.55 C17.08,14.55 11.21,14.55 11.21,14.55 C11.21,14.55 10.4,14 10.4,14 C10.4,14 17.4,14 17.4,14 C17.4,14 17.08,14.55 17.08,14.55c " />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="time_group" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="_R_G_L_2_G">
|
||||
<aapt:attr name="android:animation">
|
||||
<set android:ordering="together">
|
||||
<objectAnimator
|
||||
android:duration="434"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="0"
|
||||
android:valueFrom="73"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.667,1 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="_R_G_L_0_C_0">
|
||||
<aapt:attr name="android:animation">
|
||||
<set android:ordering="together">
|
||||
<objectAnimator
|
||||
android:duration="333"
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="0"
|
||||
android:valueFrom="M23.73 37.15 C23.73,37.15 21.71,37.15 21.71,37.15 C21.71,37.15 13.39,37.15 13.39,37.15 C13.39,37.15 -2.01,37.15 -2.01,37.15 C-2.01,37.15 -6.82,37.15 -6.82,37.15 C-6.82,37.15 -8.27,37.15 -8.27,37.15 C-8.27,37.15 -8.27,40.03 -8.27,40.03 C-8.27,40.03 23.73,40.03 23.73,40.03 C23.73,40.03 23.73,37.15 23.73,37.15c "
|
||||
android:valueTo="M23.73 37.15 C23.73,37.15 21.71,37.15 21.71,37.15 C21.71,37.15 13.39,37.15 13.39,37.15 C13.39,37.15 -2.01,37.15 -2.01,37.15 C-2.01,37.15 -6.82,37.15 -6.82,37.15 C-6.82,37.15 -8.27,37.15 -8.27,37.15 C-8.27,37.15 -8.27,40.03 -8.27,40.03 C-8.27,40.03 23.73,40.03 23.73,40.03 C23.73,40.03 23.73,37.15 23.73,37.15c "
|
||||
android:valueType="pathType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:duration="167"
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="333"
|
||||
android:valueFrom="M23.73 37.15 C23.73,37.15 21.71,37.15 21.71,37.15 C21.71,37.15 13.39,37.15 13.39,37.15 C13.39,37.15 -2.01,37.15 -2.01,37.15 C-2.01,37.15 -6.82,37.15 -6.82,37.15 C-6.82,37.15 -8.27,37.15 -8.27,37.15 C-8.27,37.15 -8.27,40.03 -8.27,40.03 C-8.27,40.03 23.73,40.03 23.73,40.03 C23.73,40.03 23.73,37.15 23.73,37.15c "
|
||||
android:valueTo="M24.11 22.78 C24.11,22.78 21.82,22.78 21.82,22.78 C21.82,22.78 15.48,20.34 14.4,20.34 C12.43,20.34 2.78,25.37 0.81,25.37 C-0.35,25.37 -5.1,22.78 -5.1,22.78 C-5.1,22.78 -7.89,22.78 -7.89,22.78 C-7.89,22.78 -7.89,39.16 -7.89,39.16 C-7.89,39.16 24.11,39.16 24.11,39.16 C24.11,39.16 24.11,22.78 24.11,22.78c "
|
||||
android:valueType="pathType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:duration="167"
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="500"
|
||||
android:valueFrom="M24.11 22.78 C24.11,22.78 21.82,22.78 21.82,22.78 C21.82,22.78 15.48,20.34 14.4,20.34 C12.43,20.34 2.78,25.37 0.81,25.37 C-0.35,25.37 -5.1,22.78 -5.1,22.78 C-5.1,22.78 -7.89,22.78 -7.89,22.78 C-7.89,22.78 -7.89,39.16 -7.89,39.16 C-7.89,39.16 24.11,39.16 24.11,39.16 C24.11,39.16 24.11,22.78 24.11,22.78c "
|
||||
android:valueTo="M24.12 9.79 C24.12,9.79 21.56,9.79 21.56,9.79 C21.56,9.79 17.22,11.4 15.06,11.4 C11.13,11.4 7.18,7.73 3.24,7.73 C0.91,7.73 -3.76,9.79 -3.76,9.79 C-3.76,9.79 -7.88,9.79 -7.88,9.79 C-7.88,9.79 -7.88,39.67 -7.88,39.67 C-7.88,39.67 24.12,39.67 24.12,39.67 C24.12,39.67 24.12,9.79 24.12,9.79c "
|
||||
android:valueType="pathType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:duration="167"
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="667"
|
||||
android:valueFrom="M24.12 9.79 C24.12,9.79 21.56,9.79 21.56,9.79 C21.56,9.79 17.22,11.4 15.06,11.4 C11.13,11.4 7.18,7.73 3.24,7.73 C0.91,7.73 -3.76,9.79 -3.76,9.79 C-3.76,9.79 -7.88,9.79 -7.88,9.79 C-7.88,9.79 -7.88,39.67 -7.88,39.67 C-7.88,39.67 24.12,39.67 24.12,39.67 C24.12,39.67 24.12,9.79 24.12,9.79c "
|
||||
android:valueTo="M24.26 0.67 C24.26,0.67 22,0.67 22,0.67 C22,0.67 15.43,-1.35 14.47,-1.35 C12.71,-1.35 2.44,1.87 0.68,1.87 C-0.36,1.87 -5.09,0.67 -5.09,0.67 C-5.09,0.67 -7.74,0.67 -7.74,0.67 C-7.74,0.67 -7.74,44.06 -7.74,44.06 C-7.74,44.06 24.26,44.06 24.26,44.06 C24.26,44.06 24.26,0.67 24.26,0.67c "
|
||||
android:valueType="pathType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:duration="133"
|
||||
android:propertyName="pathData"
|
||||
android:startOffset="833"
|
||||
android:valueFrom="M24.26 0.67 C24.26,0.67 22,0.67 22,0.67 C22,0.67 15.43,-1.35 14.47,-1.35 C12.71,-1.35 2.44,1.87 0.68,1.87 C-0.36,1.87 -5.09,0.67 -5.09,0.67 C-5.09,0.67 -7.74,0.67 -7.74,0.67 C-7.74,0.67 -7.74,44.06 -7.74,44.06 C-7.74,44.06 24.26,44.06 24.26,44.06 C24.26,44.06 24.26,0.67 24.26,0.67c "
|
||||
android:valueTo="M24.4 -9.98 C24.4,-9.98 22.38,-9.98 22.38,-9.98 C22.38,-9.98 14.07,-9.99 14.06,-9.99 C14.05,-9.99 -1.32,-9.97 -1.32,-9.97 C-1.33,-9.97 -6.14,-9.98 -6.14,-9.98 C-6.14,-9.98 -7.6,-9.98 -7.6,-9.98 C-7.6,-9.98 -7.6,44.22 -7.6,44.22 C-7.6,44.22 24.4,44.22 24.4,44.22 C24.4,44.22 24.4,-9.98 24.4,-9.98c "
|
||||
android:valueType="pathType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="time_group">
|
||||
<aapt:attr name="android:animation">
|
||||
<set android:ordering="together">
|
||||
<objectAnimator
|
||||
android:duration="1000"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="0"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
121
app/src/main/res/drawable/intro_anim_finished.xml
Normal file
@@ -0,0 +1,121 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_2_G"
|
||||
android:pivotX="53.625"
|
||||
android:pivotY="43.025"
|
||||
android:rotation="0"
|
||||
android:translateX="-37.85"
|
||||
android:translateY="6.550000000000004">
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffb300"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M9.45 18.05 C9.45,18.05 7.55,1.45 7.55,1.45 C7.55,1.45 4.65,1.75 4.65,1.75 C4.65,1.75 6.55,18.35 6.55,18.35 C6.55,18.35 9.45,18.05 9.45,18.05c M19.75 16.85 C19.75,16.85 17.85,0.25 17.85,0.25 C17.85,0.25 14.95,0.55 14.95,0.55 C14.95,0.55 16.85,17.15 16.85,17.15 C16.85,17.15 19.75,16.85 19.75,16.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_1_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M25.15 39.45 C24.25,40.55 23.55,41.25 23.45,41.35 C20.85,43.45 18.75,44.05 17.05,43.25 C14.05,41.75 14.25,36.15 14.35,35.55 C14.35,35.55 16.45,35.65 16.45,35.65 C16.35,37.25 16.65,40.65 18.05,41.35 C18.85,41.75 20.25,41.25 22.05,39.75 C22.05,39.75 27.85,33.95 26.65,29.35 C25.25,23.85 31.65,15.95 33.75,13.25 C33.75,13.25 34.05,12.95 34.05,12.95 C34.05,12.95 35.75,14.25 35.75,14.25 C35.75,14.25 35.45,14.65 35.45,14.65 C28.95,22.65 28.25,26.75 28.75,28.85 C29.75,32.85 27.05,37.15 25.15,39.45c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_2_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M3.05 17.85 C3.05,17.85 3.85,24.65 3.85,24.65 C3.85,24.65 10.15,28.85 10.15,28.85 C10.15,28.85 18.65,27.95 18.65,27.95 C18.65,27.95 23.85,22.45 23.85,22.45 C23.85,22.45 23.05,15.65 23.05,15.65 C23.05,15.65 3.05,17.85 3.05,17.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_3_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#546e7a"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M18.55 27.95 C18.55,27.95 10.05,28.85 10.05,28.85 C10.05,28.85 11.85,36.35 11.85,36.35 C11.85,36.35 18.55,35.55 18.55,35.55 C18.55,35.55 18.55,27.95 18.55,27.95c M24.65 9.95 C24.65,9.95 25.35,16.15 25.35,16.15 C25.35,16.15 0.95,18.95 0.95,18.95 C0.95,18.95 0.25,12.75 0.25,12.75 C0.25,12.75 24.65,9.95 24.65,9.95c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_1_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_1_G"
|
||||
android:translateX="-26.049"
|
||||
android:translateY="-52.150000000000006">
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00e676"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.25 C14.45,0.25 0.25,14.45 0.25,31.95 C0.25,55.85 26.95,68.35 30.15,102.45 C30.25,103.45 31.05,104.15 32.05,104.15 C33.05,104.15 33.85,103.45 33.95,102.45 C37.15,68.35 63.85,55.85 63.85,31.95 C63.65,14.35 49.45,0.25 31.95,0.25c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.95 C49.35,0.95 63.45,14.95 63.65,32.25 C63.65,32.25 63.65,31.95 63.65,31.95 C63.65,14.45 49.45,0.25 31.95,0.25 C14.45,0.25 0.25,14.35 0.25,31.95 C0.25,31.95 0.25,32.25 0.25,32.25 C0.45,14.95 14.55,0.95 31.95,0.95c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#3e2723"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M33.85 101.65 C33.75,102.65 32.95,103.35 31.95,103.35 C30.95,103.35 30.15,102.65 30.05,101.65 C26.95,67.65 0.45,55.15 0.25,31.55 C0.25,31.55 0.25,32.05 0.25,32.05 C0.25,55.95 26.95,68.45 30.15,102.55 C30.25,103.55 31.05,104.25 32.05,104.25 C33.05,104.25 33.85,103.55 33.95,102.55 C37.15,68.45 63.85,55.95 63.85,32.05 C63.85,32.05 63.85,31.55 63.85,31.55 C63.45,55.15 36.95,67.65 33.85,101.65c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_3_P_0"
|
||||
android:fillAlpha="0.45"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M24.65 16.45 C24.65,16.45 24.65,35.65 24.65,35.65 C24.65,35.65 29.85,35.65 29.85,35.65 C29.85,35.65 29.85,51.35 29.85,51.35 C29.85,51.35 42.05,30.35 42.05,30.35 C42.05,30.35 35.05,30.35 35.05,30.35 C35.05,30.35 42.05,16.35 42.05,16.35 C42.15,16.45 24.65,16.45 24.65,16.45c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_0_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_0_G"
|
||||
android:translateX="-1.3999999999999995"
|
||||
android:translateY="-35.8">
|
||||
<group android:name="_R_G_L_0_C_0_G">
|
||||
<clip-path
|
||||
android:name="_R_G_L_0_C_0"
|
||||
android:pathData="M24.4 -9.98 C24.4,-9.98 22.38,-9.98 22.38,-9.98 C22.38,-9.98 14.07,-9.99 14.06,-9.99 C14.05,-9.99 -1.32,-9.97 -1.32,-9.97 C-1.33,-9.97 -6.14,-9.98 -6.14,-9.98 C-6.14,-9.98 -7.6,-9.98 -7.6,-9.98 C-7.6,-9.98 -7.6,44.22 -7.6,44.22 C-7.6,44.22 24.4,44.22 24.4,44.22 C24.4,44.22 24.4,-9.98 24.4,-9.98c " />
|
||||
<group android:name="_R_G_L_0_C_0_G_G">
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,19.3 0,19.3 C0,19.3 5.2,19.3 5.2,19.3 C5.2,19.3 5.2,35 5.2,35 C5.2,35 17.4,14 17.4,14 C17.4,14 10.4,14 10.4,14 C10.4,14 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,0.69 0,0.69 C0,0.69 0,19.3 0,19.3 C0,19.3 0.55,19.3 0.55,19.3 C0.55,19.3 0.55,0.69 0.55,0.69 C0.55,0.69 17.05,0.69 17.05,0.69 C17.05,0.69 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M5.82 33.92 C5.82,33.92 5.2,35 5.2,35 C5.2,35 5.2,19.3 5.2,19.3 C5.2,19.3 5.82,19.3 5.82,19.3 C5.82,19.3 5.82,33.92 5.82,33.92c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_3_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M17.08 14.55 C17.08,14.55 11.21,14.55 11.21,14.55 C11.21,14.55 10.4,14 10.4,14 C10.4,14 17.4,14 17.4,14 C17.4,14 17.08,14.55 17.08,14.55c " />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="time_group" />
|
||||
</vector>
|
||||
120
app/src/main/res/drawable/intro_static.xml
Normal file
@@ -0,0 +1,120 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_2_G"
|
||||
android:pivotX="53.625"
|
||||
android:pivotY="43.025"
|
||||
android:rotation="0"
|
||||
android:translateX="-37.85"
|
||||
android:translateY="6.550000000000004">
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffb300"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M9.45 18.05 C9.45,18.05 7.55,1.45 7.55,1.45 C7.55,1.45 4.65,1.75 4.65,1.75 C4.65,1.75 6.55,18.35 6.55,18.35 C6.55,18.35 9.45,18.05 9.45,18.05c M19.75 16.85 C19.75,16.85 17.85,0.25 17.85,0.25 C17.85,0.25 14.95,0.55 14.95,0.55 C14.95,0.55 16.85,17.15 16.85,17.15 C16.85,17.15 19.75,16.85 19.75,16.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_1_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M25.15 39.45 C24.25,40.55 23.55,41.25 23.45,41.35 C20.85,43.45 18.75,44.05 17.05,43.25 C14.05,41.75 14.25,36.15 14.35,35.55 C14.35,35.55 16.45,35.65 16.45,35.65 C16.35,37.25 16.65,40.65 18.05,41.35 C18.85,41.75 20.25,41.25 22.05,39.75 C22.05,39.75 27.85,33.95 26.65,29.35 C25.25,23.85 31.65,15.95 33.75,13.25 C33.75,13.25 34.05,12.95 34.05,12.95 C34.05,12.95 35.75,14.25 35.75,14.25 C35.75,14.25 35.45,14.65 35.45,14.65 C28.95,22.65 28.25,26.75 28.75,28.85 C29.75,32.85 27.05,37.15 25.15,39.45c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_2_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#90a4ae"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M3.05 17.85 C3.05,17.85 3.85,24.65 3.85,24.65 C3.85,24.65 10.15,28.85 10.15,28.85 C10.15,28.85 18.65,27.95 18.65,27.95 C18.65,27.95 23.85,22.45 23.85,22.45 C23.85,22.45 23.05,15.65 23.05,15.65 C23.05,15.65 3.05,17.85 3.05,17.85c " />
|
||||
<path
|
||||
android:name="_R_G_L_2_G_D_3_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#546e7a"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M18.55 27.95 C18.55,27.95 10.05,28.85 10.05,28.85 C10.05,28.85 11.85,36.35 11.85,36.35 C11.85,36.35 18.55,35.55 18.55,35.55 C18.55,35.55 18.55,27.95 18.55,27.95c M24.65 9.95 C24.65,9.95 25.35,16.15 25.35,16.15 C25.35,16.15 0.95,18.95 0.95,18.95 C0.95,18.95 0.25,12.75 0.25,12.75 C0.25,12.75 24.65,9.95 24.65,9.95c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_1_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_1_G"
|
||||
android:translateX="-26.049"
|
||||
android:translateY="-52.150000000000006">
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00e676"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.25 C14.45,0.25 0.25,14.45 0.25,31.95 C0.25,55.85 26.95,68.35 30.15,102.45 C30.25,103.45 31.05,104.15 32.05,104.15 C33.05,104.15 33.85,103.45 33.95,102.45 C37.15,68.35 63.85,55.85 63.85,31.95 C63.65,14.35 49.45,0.25 31.95,0.25c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M31.95 0.95 C49.35,0.95 63.45,14.95 63.65,32.25 C63.65,32.25 63.65,31.95 63.65,31.95 C63.65,14.45 49.45,0.25 31.95,0.25 C14.45,0.25 0.25,14.35 0.25,31.95 C0.25,31.95 0.25,32.25 0.25,32.25 C0.45,14.95 14.55,0.95 31.95,0.95c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#3e2723"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M33.85 101.65 C33.75,102.65 32.95,103.35 31.95,103.35 C30.95,103.35 30.15,102.65 30.05,101.65 C26.95,67.65 0.45,55.15 0.25,31.55 C0.25,31.55 0.25,32.05 0.25,32.05 C0.25,55.95 26.95,68.45 30.15,102.55 C30.25,103.55 31.05,104.25 32.05,104.25 C33.05,104.25 33.85,103.55 33.95,102.55 C37.15,68.45 63.85,55.95 63.85,32.05 C63.85,32.05 63.85,31.55 63.85,31.55 C63.45,55.15 36.95,67.65 33.85,101.65c " />
|
||||
<path
|
||||
android:name="_R_G_L_1_G_D_3_P_0"
|
||||
android:fillAlpha="0.45"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M24.65 16.45 C24.65,16.45 24.65,35.65 24.65,35.65 C24.65,35.65 29.85,35.65 29.85,35.65 C29.85,35.65 29.85,51.35 29.85,51.35 C29.85,51.35 42.05,30.35 42.05,30.35 C42.05,30.35 35.05,30.35 35.05,30.35 C35.05,30.35 42.05,16.35 42.05,16.35 C42.15,16.45 24.65,16.45 24.65,16.45c " />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="_R_G_L_0_G_N_1_T_0"
|
||||
android:translateX="94"
|
||||
android:translateY="96">
|
||||
<group
|
||||
android:name="_R_G_L_0_G"
|
||||
android:translateX="-1.3999999999999995"
|
||||
android:translateY="-35.8">
|
||||
<group android:name="_R_G_L_0_C_0_G">
|
||||
<clip-path
|
||||
android:name="_R_G_L_0_C_0"
|
||||
android:pathData="M23.73 37.15 C23.73,37.15 21.71,37.15 21.71,37.15 C21.71,37.15 13.39,37.15 13.39,37.15 C13.39,37.15 -2.01,37.15 -2.01,37.15 C-2.01,37.15 -6.82,37.15 -6.82,37.15 C-6.82,37.15 -8.27,37.15 -8.27,37.15 C-8.27,37.15 -8.27,40.03 -8.27,40.03 C-8.27,40.03 23.73,40.03 23.73,40.03 C23.73,40.03 23.73,37.15 23.73,37.15c " />
|
||||
<group android:name="_R_G_L_0_C_0_G_G">
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_0_P_0"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,19.3 0,19.3 C0,19.3 5.2,19.3 5.2,19.3 C5.2,19.3 5.2,35 5.2,35 C5.2,35 17.4,14 17.4,14 C17.4,14 10.4,14 10.4,14 C10.4,14 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_1_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M0 0.1 C0,0.1 0,0.69 0,0.69 C0,0.69 0,19.3 0,19.3 C0,19.3 0.55,19.3 0.55,19.3 C0.55,19.3 0.55,0.69 0.55,0.69 C0.55,0.69 17.05,0.69 17.05,0.69 C17.05,0.69 17.4,0 17.4,0 C17.5,0.1 0,0.1 0,0.1c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_2_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M5.82 33.92 C5.82,33.92 5.2,35 5.2,35 C5.2,35 5.2,19.3 5.2,19.3 C5.2,19.3 5.82,19.3 5.82,19.3 C5.82,19.3 5.82,33.92 5.82,33.92c " />
|
||||
<path
|
||||
android:name="_R_G_L_0_G_G_0_D_3_P_0"
|
||||
android:fillAlpha="0.2"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData=" M17.08 14.55 C17.08,14.55 11.21,14.55 11.21,14.55 C11.21,14.55 10.4,14 10.4,14 C10.4,14 17.4,14 17.4,14 C17.4,14 17.08,14.55 17.08,14.55c " />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||