Compare commits

..

103 Commits
0.9.1 ... 1.1.1

Author SHA1 Message Date
johan12345
90254915e3 Release 1.1.1 2021-11-01 13:10:37 +01:00
johan12345
b7f56ecff4 fix detection of imperial units when locale is "unspecified English" 2021-11-01 13:03:50 +01:00
johan12345
fa3910d3c8 avoid NPE when country == null 2021-11-01 12:47:05 +01:00
johan12345
4500c55560 Android Auto: throttle updates of distances
to mitigate bug in AA https://issuetracker.google.com/issues/204692002
2021-11-01 12:39:59 +01:00
johan12345
a493e1a548 Android Auto MapScreen: show distances in correct units 2021-11-01 12:03:27 +01:00
johan12345
ddaab42e45 update filteredConnectors before chargepoints
fixes incorrect marker colors
2021-11-01 10:37:46 +01:00
johan12345
9f50341ab7 use latest Google Maps renderer 2021-11-01 10:26:30 +01:00
johan12345
9966b44a76 Release 1.1.0 2021-10-17 21:29:53 +02:00
johan12345
d44b2206d2 update build status badge 2021-10-17 15:49:56 +02:00
johan12345
f61082f491 fix incorrect formatting in api_keys.md 2021-10-17 15:27:01 +02:00
johan12345
f58d96c939 fix links in README 2021-10-17 15:26:08 +02:00
Johan von Forstner
29aedfa3d9 Merge pull request #140 from johan12345/contributing-docs
Improve docs for first-time contributors
2021-10-17 15:25:15 +02:00
johan12345
8331f92f10 Improve docs for first-time contributors
adds two docs pages with info on API keys and Android Auto testing.
fixes #112
2021-10-17 15:24:50 +02:00
johan12345
123680d3e8 replace Mapbox android-plugin-places with just the Java SDK
(the UI has been replaced by our own one in #120)
2021-10-17 12:43:16 +02:00
johan12345
0f6b45d745 upgrade navigation component to 2.4.0-alpha10
(necessary for Android 12 compatibility of deep links)
2021-10-17 12:11:28 +02:00
Johan von Forstner
69faa94f18 Merge pull request #139 from pt2121/pt/safeArgs
refactor to use Android Navigation Component's SafeArgs
2021-10-17 12:10:16 +02:00
prat t
70805b7960 refactor to use Navigation Component's SafeArgs 2021-10-16 15:16:56 -07:00
johan12345
56453b0658 remove obsolete TODO 2021-10-16 15:51:36 +02:00
johan12345
975d95e37e refactor app settings into separate sub-screens
for better overview
2021-10-16 15:49:00 +02:00
johan12345
ba34cd016a fix static splashscreen icon size
after upgrade to core-splashscreen 1.0.0-alpha02 in e2bcf8d1
2021-10-16 12:36:29 +02:00
johan12345
590b16aa49 make debug build distinguishable from release build
with grayscale app icon + different label
fixes #113
2021-10-16 12:25:26 +02:00
johan12345
5fe8d0cab4 replace Google Maps v3 beta with Play Services version 17.0.1
fixes #124
fixes #30
2021-10-16 12:16:27 +02:00
johan12345
9d7b181410 remove focus from search when selecting a charger 2021-10-10 17:55:16 +02:00
johan12345
128532aac6 open autocomplete list more quickly 2021-10-10 17:51:54 +02:00
johan12345
486854f56c Android Auto: use slightly darker color for >100 kW chargers for better contrast 2021-10-09 19:16:21 +02:00
johan12345
1e30db5cd1 GoingElectricApi: map plug types correctly to names 2021-10-09 13:55:25 +02:00
johan12345
aad386ab04 MultiSelectDialog: put common choices on top even when selected 2021-10-09 13:44:17 +02:00
johan12345
e2bcf8d1cd upgrade dependencies 2021-10-08 22:02:04 +02:00
Johan von Forstner
f56fad1282 Merge pull request #134 from johan12345/filter-by-favorites
add possibility to show only the favorites on a map
2021-10-08 21:51:46 +02:00
johan12345
adb4d938cc add possibility to show only the favorites on a map (fixes #119) 2021-10-08 21:51:13 +02:00
Johan von Forstner
b773f65912 Update screenshot URLs 2021-10-06 08:39:13 +02:00
johan12345
de335b18d8 PlaceAutocompleteAdapter: do not modify resultList on background thread 2021-10-05 21:43:47 +02:00
johan12345
6c8380b8ce revisit Android Auto location service connection
to possibly fix IllegalArgumentException: Service not registered
based on https://stackoverflow.com/questions/22079909/android-java-lang-illegalargumentexception-service-not-registered
2021-10-05 21:12:03 +02:00
Johan von Forstner
81afdca19d Merge pull request #130 from johan12345/highlight_favorites
highlight favorites on map
2021-10-03 13:33:12 +02:00
johan12345
14e03ba6dd highlight favorites on map
fixes #118
2021-10-03 13:31:58 +02:00
Johan von Forstner
abe12b45c3 disable git LFS for screenshots
(not supported on F-Droid
2021-10-03 12:55:11 +02:00
johan12345
23387ae371 Release 1.0.0 2021-10-02 14:08:44 +02:00
johan12345
25f466b6d7 Merge branch 'place-search-recents' 2021-10-02 13:49:25 +02:00
johan12345
6692b21bf9 add non-dark Mapbox logo 2021-10-02 13:48:50 +02:00
johan12345
5959fe8be4 limit number of autocomplete results 2021-10-02 13:40:22 +02:00
johan12345
00f4c13fcc add button to delete search history 2021-10-02 13:33:51 +02:00
johan12345
47054d470b replace deprecated method getAdapterPosition 2021-10-02 13:23:30 +02:00
Johan von Forstner
d10192cae1 save recent place search results
fixes #128
2021-10-02 13:19:33 +02:00
johan12345
e1b90955c3 handle back button correctly
fixes #115
2021-10-02 13:18:27 +02:00
johan12345
d249bf47c7 improve title of filter profiles editing 2021-10-02 13:15:58 +02:00
johan12345
738dcd5f8d do not allow blank names for filter profiles 2021-10-02 13:11:05 +02:00
johan12345
ad4f32ec32 fix missing filter name in title of FilterFragment 2021-10-02 12:47:22 +02:00
Johan von Forstner
4d03107ae7 add links to translated FAQ and privacy policy
#110
2021-09-26 16:17:44 +02:00
Johan von Forstner
0e93e310bf MultiSelectDialog: move selected entries to the top
fixes #126
2021-09-26 16:17:44 +02:00
Johan von Forstner
6cb8940696 Catch IllegalArgumentException in navigation library 2021-09-26 14:47:55 +02:00
johan12345
dad30eb51e Release 1.0.0-beta04 2021-09-19 19:37:52 +02:00
johan12345
abf6a2b933 upgrade some dependencies 2021-09-19 19:28:26 +02:00
johan12345
2c5685d918 Android Auto: use smartphone location even with car API level 3
if car hardware location is not available
2021-09-19 19:25:59 +02:00
johan12345
b61e57b022 Android Auto: fix crash when filter profile has no name 2021-09-19 19:25:59 +02:00
Johan von Forstner
e6428cc8db Merge pull request #125 from johan12345/android-12
Update to Android 12 targetSdkVersion and implement corresponding changes
2021-09-12 21:39:39 +02:00
johan12345
6302006a35 Android 12 compat: implement new SplashScreen API
with animated icon
update onboarding to avoid showing animation twice
2021-09-12 21:21:17 +02:00
johan12345
ab93577a98 Android 12 compat: support for approximate location permission (#123) 2021-09-12 21:21:17 +02:00
johan12345
98b695ed4b Android 12 compat: set exported attribute explicitly (#123)
for services and activities in Manifest
2021-09-12 18:48:15 +02:00
johan12345
ed8cb50b08 increase targetSdk to Android 12 (API 31) (#123) 2021-09-12 18:48:14 +02:00
johan12345
88d89c2760 fix crash in FavoritesFragment.onDestroy
apparently it can be called before onCreate?
2021-09-12 18:00:07 +02:00
johan12345
80c25cb416 build.gradle: add possibility to pass encrypted Mapbox API Key 2021-09-12 12:23:25 +02:00
johan12345
81c8e54dd2 Release 1.0.0-beta03 2021-09-12 11:49:09 +02:00
johan12345
8c01ee1581 shorten error message on Android Auto 2021-09-12 11:48:28 +02:00
johan12345
e8db5acfbf Check if Android Auto Version >= 6.7 before using Car API Level 3 2021-09-12 11:46:15 +02:00
johan12345
f6bb3c03ba Release 1.0.0-beta02
(just to fix a crash caused by Lottie)
2021-09-11 22:29:20 +02:00
johan12345
134f3856b9 upgrade Lottie 2021-09-11 22:23:30 +02:00
johan12345
4974cc6d83 Release 1.0.0-beta01 2021-09-11 11:53:13 +02:00
Johan von Forstner
edd072b83a Merge pull request #101 from johan12345/car-app-library-1.1.0
Update Android Auto to Car App Library 1.1.0, Adds vehicle data and Chargeprice integration
2021-09-11 11:43:27 +02:00
Johan von Forstner
35ddda5bfe Update screenshots in README 2021-09-11 11:16:09 +02:00
johan12345
8b241e3f6f new screenshots
- English and German
- Mapbox and Google Maps
- + Android Auto
2021-09-11 11:09:08 +02:00
johan12345
b3c5fe788d suppress lint error 2021-09-11 09:54:30 +02:00
johan12345
6fd737f6e9 Android Auto: Disable Chargeprice in unsupported countries
see also: cf6c6628, #117
2021-09-11 09:54:30 +02:00
johan12345
08cd4eb849 Android Auto: do not update the map when location changes
to avoid running into template restrictions
2021-09-11 09:54:30 +02:00
johan12345
ff75594b37 get CarHardwareManager lazily 2021-09-11 09:54:30 +02:00
johan12345
2576bc4854 upgrade compileSdk to Android 12
required for new car app library
2021-09-11 09:54:30 +02:00
johan12345
b2c29b647b upgrade car app library to 1.1.0-beta01 2021-09-11 09:54:30 +02:00
johan12345
2167a63321 only use ConstraintManager if car API level >= 2
refs a562ee6c
2021-09-11 09:54:30 +02:00
johan12345
fb0a2cfa1c internal test release 2021-09-11 09:54:30 +02:00
johan12345
07be77c573 ChargepriceScreen: fix showing error messages 2021-09-11 09:54:30 +02:00
johan12345
ae0a84db4c VehicleDataScreen: setup listeners with lifecycle events 2021-09-11 09:54:30 +02:00
johan12345
dc5ffb148d Chargeprice: check car API level 2021-09-11 09:54:30 +02:00
johan12345
066b7c085e add link to Chargeprice website 2021-09-11 09:54:30 +02:00
johan12345
4ae16df064 add Chargeprice icon 2021-09-11 09:54:30 +02:00
johan12345
17a40127e6 add Chargeprice to Android Auto
fixes #80
2021-09-11 09:54:30 +02:00
johan12345
31ad748796 use car hardware location data if available 2021-09-11 09:54:30 +02:00
johan12345
fe4db38798 show vehicle data screen only if API level available 2021-09-11 09:54:30 +02:00
johan12345
6c2243078b Vehicle data screen: Add speed and range + gauge icons 2021-09-11 09:54:30 +02:00
johan12345
71f1ee8d7b make VehicleDataScreen request permissions and work correctly 2021-09-11 09:54:30 +02:00
johan12345
ab0c37cb82 make PermissionScreen reusable 2021-09-11 09:54:30 +02:00
Johan von Forstner
65189cd798 Android Auto: create a VehicleDataScreen showing state of charge 2021-09-11 09:54:30 +02:00
Johan von Forstner
630178bfcf Update car app library to 1.1.0-alpha02 2021-09-11 09:54:30 +02:00
Johan von Forstner
bcee975124 remove now unneeded @ExperimentalCarApi annotations 2021-09-11 09:54:30 +02:00
Johan von Forstner
04fc17d73c increase image size corresponding to updated Android Auto docs 2021-09-11 09:54:30 +02:00
Johan von Forstner
139c02ef70 use ConstraintManager to dynamically get maximum number of items to be shown on Android Auto list 2021-09-11 09:54:30 +02:00
Johan von Forstner
88a8520f27 use CarAppService.requestPermission() instead of custom PermissionActivity 2021-09-11 09:54:30 +02:00
johan12345
4f3157a0ac update Car app library to 1.1.0-alpha1 2021-09-11 09:54:30 +02:00
johan12345
17d57729b3 remove Android Auto screenshots from F-Droid 2021-09-11 09:49:40 +02:00
johan12345
1f3df2e0bf update F-Droid description, removing features that are not available in the F-Droid version
(Google Maps data, Android Auto)
2021-09-11 09:49:09 +02:00
johan12345
e2e95ce85d fix NPE in AvailabilityDetector 2021-09-06 20:34:20 +02:00
johan12345
d79b554dcc remove unused nav graph destination 2021-09-06 20:18:25 +02:00
johan12345
98e91ea3db FavoritesFramgent: create locationClient in onCreate, not onCreateView 2021-09-05 17:02:45 +02:00
johan12345
b8c8245978 Android Auto: avoid unnecessary location updates 2021-09-05 16:47:22 +02:00
johan12345
fd1f05888a fix IndexOutOfBoundsException 2021-09-05 15:01:46 +02:00
154 changed files with 2981 additions and 801 deletions

2
.gitattributes vendored Normal file
View 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

View File

@@ -1,4 +1,4 @@
EVMap [![Build Status](https://travis-ci.org/johan12345/EVMap.svg?branch=master)](https://travis-ci.org/johan12345/EVMap)
EVMap [![Build Status](https://app.travis-ci.com/johan12345/EVMap.svg?branch=master)](https://app.travis-ci.com/johan12345/EVMap)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
@@ -28,38 +28,22 @@ Features
Screenshots
-----------
<img src="https://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.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -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"

View File

Binary file not shown.

View File

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

View File

@@ -5,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>

View File

@@ -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 {

View File

@@ -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
}
}

View 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
}
}

View File

@@ -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()
}

View File

@@ -7,7 +7,9 @@ import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import net.vonforst.evmap.R
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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
}

View 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
}
}

View File

@@ -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()
}

View File

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

View File

@@ -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
)
}
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

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

View File

@@ -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? {

View File

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

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -20,15 +21,10 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -84,8 +80,9 @@ class ChargepriceFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val charger = requireArguments().getParcelable<ChargeLocation>(ARG_CHARGER)!!
val dataSource = requireArguments().getString(ARG_DATASOURCE)!!
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
val charger = fragmentArgs.charger
val dataSource = fragmentArgs.dataSource
vm.charger.value = charger
vm.dataSource.value = dataSource
if (vm.chargepoint.value == null) {
@@ -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")
}
)
}
}
}
}

View File

@@ -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()
}
}

View File

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

View File

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

View File

@@ -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()
}

View File

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

View File

@@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`))");
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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"
}
}
}

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View 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>

View 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>

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