mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-27 00:57:45 -05:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e4167689d | ||
|
|
8a2ad55dd6 | ||
|
|
44ce0cfaea | ||
|
|
70f964549e | ||
|
|
c045eed41a | ||
|
|
3ded108c3c | ||
|
|
b3eb1e31e8 | ||
|
|
7eeb10faca | ||
|
|
4208e1a4b5 | ||
|
|
54004f14b5 | ||
|
|
8eabff4888 | ||
|
|
d5b5337aeb | ||
|
|
913d8a00cf | ||
|
|
fc5003cd31 | ||
|
|
ff96e49ead | ||
|
|
36bd74e091 | ||
|
|
3d0dc16f49 | ||
|
|
41b374350b | ||
|
|
fc72044b82 | ||
|
|
e96fcd4a88 | ||
|
|
36f34bde1e | ||
|
|
624c5d8f92 | ||
|
|
7fcb187dda | ||
|
|
7188a2aa64 | ||
|
|
cf6c662832 | ||
|
|
4ceef7997d | ||
|
|
3f79bdd125 | ||
|
|
fb279f90c5 | ||
|
|
6f35ced260 | ||
|
|
c967bab524 | ||
|
|
6bf80e2b49 | ||
|
|
d97cb4b9fb | ||
|
|
17eaeb99da | ||
|
|
beebbe1c1b | ||
|
|
0a2bbd5fb4 | ||
|
|
7f1f4b67a1 | ||
|
|
d5e29a5112 | ||
|
|
77f478c9e0 | ||
|
|
1008a2c2cd | ||
|
|
2219e2fe27 | ||
|
|
8ce145a9af | ||
|
|
b799dae28b | ||
|
|
07a482a6b6 | ||
|
|
4f1253b201 | ||
|
|
8bc4a7ae40 | ||
|
|
d686becfe4 | ||
|
|
a686c51b32 | ||
|
|
382ead9e08 | ||
|
|
2da7ea4c05 | ||
|
|
20c4274c55 | ||
|
|
748212189f | ||
|
|
d86a49beb7 | ||
|
|
f8b1a20d1a | ||
|
|
14edb6f0cd | ||
|
|
7726088f91 | ||
|
|
cbc7c5a6d8 | ||
|
|
d510d81914 | ||
|
|
9f5abd6c91 | ||
|
|
966f62ac3d | ||
|
|
91caf40bdb | ||
|
|
72c0293365 | ||
|
|
ca9dc9629f | ||
|
|
438e529257 | ||
|
|
5f69123d89 | ||
|
|
cf421b52a8 | ||
|
|
1b049d35b8 | ||
|
|
f6690a3566 | ||
|
|
cc97020216 | ||
|
|
0e1e3ba46e | ||
|
|
657c209827 | ||
|
|
6ec44bb526 | ||
|
|
0943505d90 | ||
|
|
f155f7615f | ||
|
|
e8850575f2 | ||
|
|
d1c4d0a621 | ||
|
|
ecf27abdc5 | ||
|
|
5f5142baa6 | ||
|
|
fa53a9fc5a | ||
|
|
9a0a7b4e5f | ||
|
|
1a43703db5 | ||
|
|
459589c51f | ||
|
|
9393fe7380 | ||
|
|
f62bd1c3c4 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: johan12345
|
||||
custom: 'https://paypal.me/johan98'
|
||||
@@ -1,5 +1,5 @@
|
||||
language: java
|
||||
dist: trusty
|
||||
dist: focal
|
||||
env:
|
||||
global:
|
||||
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
|
||||
|
||||
@@ -2,7 +2,7 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
addressable (2.7.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.0)
|
||||
@@ -125,7 +125,7 @@ GEM
|
||||
naturally (2.2.0)
|
||||
os (1.1.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (4.0.5)
|
||||
public_suffix (4.0.6)
|
||||
rake (13.0.1)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
@@ -154,6 +154,7 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
@@ -170,6 +171,7 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw32
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
13
README.md
13
README.md
@@ -3,7 +3,7 @@ EVMap [
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
|
||||
|
||||
Android app to access the goingelectric.de electric vehicle charging station directory.
|
||||
Android app to find electric vehicle charging stations.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=net.vonforst.evmap" target="_blank">
|
||||
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="100"/></a>
|
||||
@@ -14,11 +14,12 @@ Features
|
||||
--------
|
||||
|
||||
- [Material Design](https://material.io/)
|
||||
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) directory
|
||||
- Realtime availability information (beta)
|
||||
- Search places
|
||||
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) and [Open Charge Map](https://openchargemap.org) directories
|
||||
- Realtime availability information (only in Europe)
|
||||
- Search for places
|
||||
- Advanced filtering options, including saved filter profiles
|
||||
- Favorites list, also with availability information
|
||||
- Charging price comparison, powered by [Chargeprice.app](https://chargeprice.app)
|
||||
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
|
||||
- Android Auto integration
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
@@ -59,6 +60,6 @@ following content:
|
||||
</string>
|
||||
<string name="openchargemap_key" translatable="false">
|
||||
insert your OpenChargeMap key here
|
||||
</string>
|
||||
</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
25
_img/connectors/connector_ccs_typ1.svg
Normal file
25
_img/connectors/connector_ccs_typ1.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_5" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:1.7;stroke-miterlimit:10;}
|
||||
.st2{fill:none;stroke:#000000;stroke-width:0.5;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<circle cx="9" cy="18.7" r="1.4" />
|
||||
<circle cx="15" cy="18.7" r="1.4" />
|
||||
<path class="st0" d="M8.9,16.1h6.2c1.5,0,2.7,1.2,2.7,2.7l0,0c0,1.5-1.2,2.7-2.7,2.7H8.9c-1.5,0-2.7-1.2-2.7-2.7l0,0
|
||||
C6.2,17.3,7.4,16.1,8.9,16.1z" />
|
||||
<g>
|
||||
<circle cx="14.7" cy="6.4" r="1.3" />
|
||||
<circle cx="15.3" cy="10.5" r="0.8" />
|
||||
<circle cx="8.7" cy="10.5" r="0.8" />
|
||||
<circle cx="9.3" cy="6.4" r="1.3" />
|
||||
<circle cx="12" cy="13.1" r="1.3" />
|
||||
<circle class="st1" cx="12" cy="9.1" r="6.3" />
|
||||
<rect x="11" y="15.4" width="2" height="1.3" />
|
||||
<line class="st2" x1="10.9" y1="1.3" x2="13.1" y2="1.3" />
|
||||
<polygon points="13.1,0.9 13.1,2.4 14.5,3.1 13.8,1 " />
|
||||
<polygon points="10.9,0.9 10.9,2.4 9.5,3.1 10.2,1 " />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 48
|
||||
versionName "0.8.0"
|
||||
versionCode 57
|
||||
versionName "0.9.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -112,7 +112,7 @@ dependencies {
|
||||
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.0.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
@@ -125,7 +125,7 @@ dependencies {
|
||||
implementation 'moe.banana:moshi-jsonapi:3.5.0'
|
||||
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0'
|
||||
implementation 'io.coil-kt:coil:1.1.0'
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3'
|
||||
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'
|
||||
@@ -139,7 +139,7 @@ dependencies {
|
||||
googleImplementation 'androidx.car.app:app:1.0.0'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '1f050d860f'
|
||||
def anyMapsVersion = '95ddd6c083'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
@@ -157,6 +157,7 @@ dependencies {
|
||||
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'
|
||||
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
|
||||
@@ -185,6 +186,12 @@ dependencies {
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
// ACRA (crash reporting)
|
||||
def acraVersion = "5.8.4"
|
||||
implementation("ch.acra:acra-mail:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
|
||||
@@ -1,49 +1,5 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.mapbox.geojson.BoundingBox
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.mapboxsdk.plugins.places.autocomplete.PlaceAutocomplete
|
||||
import com.mapbox.mapboxsdk.plugins.places.autocomplete.model.PlaceOptions
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
|
||||
import net.vonforst.evmap.viewmodel.PlaceWithBounds
|
||||
|
||||
|
||||
fun launchAutocomplete(fragment: Fragment) {
|
||||
val placeOptions = PlaceOptions.builder()
|
||||
.build(PlaceOptions.MODE_CARDS)
|
||||
|
||||
val intent = PlaceAutocomplete.IntentBuilder()
|
||||
.accessToken(fragment.getString(R.string.mapbox_key))
|
||||
.placeOptions(placeOptions)
|
||||
.build(fragment.requireActivity())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
// show keyboard
|
||||
val imm = fragment.requireContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
|
||||
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
|
||||
val place = PlaceAutocomplete.getPlace(intent) ?: return null
|
||||
val bbox = place.bbox()?.toLatLngBounds()
|
||||
val center = place.center()!!.toLatLng()
|
||||
return PlaceWithBounds(center, bbox)
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
return LatLngBounds(
|
||||
southwest().toLatLng(),
|
||||
northeast().toLatLng()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Point.toLatLng(): LatLng = LatLng(this.latitude(), this.longitude())
|
||||
fun getAutocompleteProviders(context: Context) = listOf(MapboxAutocompleteProvider(context))
|
||||
@@ -27,14 +27,16 @@ class DonateFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
binding.toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
binding.btnDonate.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
</resources>
|
||||
@@ -6,6 +6,13 @@
|
||||
<string-array name="pref_map_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
|
||||
<string name="donate_paypal">Donate with PayPal</string>
|
||||
|
||||
@@ -4,62 +4,24 @@ import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.ResultReceiver
|
||||
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.Session
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.model.Distance.UNIT_KILOMETERS
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.GEReferenceDataRepository
|
||||
import net.vonforst.evmap.storage.OCMReferenceDataRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
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) {
|
||||
@@ -76,6 +38,7 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
@@ -158,566 +121,3 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
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
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen to grant location permission
|
||||
*/
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
.setTitle(carContext.getString(R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.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()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener {
|
||||
carContext.finishCarApp()
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
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
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastUpdateLocation: Location? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
|
||||
HashMap()
|
||||
private val maxRows = 6
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.BACK)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation): Row {
|
||||
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
setTitle(charger.name)
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distance = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
) / 1000
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(Distance.create(distance, UNIT_KILOMETERS)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${charger.maxPower.roundToInt()} kW")
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (lastUpdateLocation == null ||
|
||||
location.distanceTo(lastUpdateLocation) > updateThreshold
|
||||
) {
|
||||
lastUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers(location)
|
||||
}
|
||||
}
|
||||
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
|
||||
private fun loadChargers(location: Location) {
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
if (favorites) {
|
||||
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
null
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 5,
|
||||
zoom = 16f,
|
||||
emptyList()
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) > availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
chargers?.take(maxRows)?.map {
|
||||
lifecycleScope.async {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
val date = ZonedDateTime.now()
|
||||
val availability = getAvailability(it).data
|
||||
if (availability != null) {
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getReferenceData(): ReferenceData {
|
||||
val api = api
|
||||
return when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (charger == null) loadCharger()
|
||||
|
||||
return PaneTemplate.Builder(
|
||||
Pane.Builder().apply {
|
||||
charger?.let { charger ->
|
||||
addRow(Row.Builder().apply {
|
||||
setTitle(charger.address.toString())
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
addText(chargepointsText)
|
||||
}.build())
|
||||
addRow(Row.Builder().apply {
|
||||
photo?.let {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = StringBuilder().apply {
|
||||
charger.operator?.let { append(it) }
|
||||
charger.network?.let {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.faultReport?.created?.let {
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
it.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*val types = charger.chargepoints.map { it.type }.distinct()
|
||||
if (types.size == 1) {
|
||||
setImage(
|
||||
CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))),
|
||||
Row.IMAGE_TYPE_ICON)
|
||||
}*/
|
||||
}.build())
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
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()
|
||||
)
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
|
||||
charger = response.data!!
|
||||
|
||||
val photo = charger?.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
this@ChargerDetailScreen.photo =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
}
|
||||
|
||||
availability = charger?.let { getAvailability(it).data }
|
||||
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getReferenceData(): ReferenceData {
|
||||
val api = api
|
||||
return when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
|
||||
|
||||
return if (unknown) {
|
||||
CarColor.DEFAULT
|
||||
} else if (available > 0) {
|
||||
CarColor.GREEN
|
||||
} else if (allFaulted) {
|
||||
CarColor.RED
|
||||
} else {
|
||||
CarColor.BLUE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
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.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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.createApi
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.getReferenceData
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
|
||||
|
||||
init {
|
||||
referenceData.observe(this) {
|
||||
loadCharger()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (charger == null) loadCharger()
|
||||
|
||||
return PaneTemplate.Builder(
|
||||
Pane.Builder().apply {
|
||||
charger?.let { charger ->
|
||||
addRow(Row.Builder().apply {
|
||||
setTitle(charger.address.toString())
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
addText(chargepointsText)
|
||||
}.build())
|
||||
addRow(Row.Builder().apply {
|
||||
photo?.let {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = StringBuilder().apply {
|
||||
charger.operator?.let { append(it) }
|
||||
charger.network?.let {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.faultReport?.created?.let {
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
it.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*val types = charger.chargepoints.map { it.type }.distinct()
|
||||
if (types.size == 1) {
|
||||
setImage(
|
||||
CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))),
|
||||
Row.IMAGE_TYPE_ICON)
|
||||
}*/
|
||||
}.build())
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
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()
|
||||
)
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
val referenceData = referenceData.value ?: return
|
||||
lifecycleScope.launch {
|
||||
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
|
||||
if (response.status == Status.SUCCESS) {
|
||||
charger = response.data!!
|
||||
|
||||
val photo = charger?.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
this@ChargerDetailScreen.photo =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
}
|
||||
|
||||
availability = charger?.let { getAvailability(it).data }
|
||||
|
||||
invalidate()
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
Normal file
90
app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
Normal file
@@ -0,0 +1,90 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
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_DISABLED
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
private val maxRows = 6
|
||||
private val checkIcon =
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check)).build()
|
||||
private val emptyIcon: CarIcon
|
||||
|
||||
init {
|
||||
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
|
||||
emptyIcon = CarIcon.Builder(
|
||||
IconCompat.createWithBitmap(
|
||||
Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
init {
|
||||
filterProfiles.observe(this) {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
filterProfiles.value?.let {
|
||||
setSingleList(buildFilterProfilesList(it.take(maxRows), prefs.filterStatus))
|
||||
} ?: setLoading(true)
|
||||
setTitle(carContext.getString(R.string.menu_filter))
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildFilterProfilesList(
|
||||
profiles: List<FilterProfile>,
|
||||
filterStatus: Long
|
||||
): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.no_filters))
|
||||
if (FILTERS_DISABLED == filterStatus) {
|
||||
setImage(checkIcon)
|
||||
} else {
|
||||
setImage(emptyIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
profiles.forEach {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(it.name)
|
||||
if (it.id == filterStatus) {
|
||||
setImage(checkIcon)
|
||||
} else {
|
||||
setImage(emptyIcon)
|
||||
}
|
||||
setOnClickListener {
|
||||
prefs.filterStatus = it.id
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state))
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
313
app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
Normal file
313
app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
Normal file
@@ -0,0 +1,313 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
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.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.createApi
|
||||
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.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
||||
import net.vonforst.evmap.viewmodel.getFilterValues
|
||||
import net.vonforst.evmap.viewmodel.getFilters
|
||||
import net.vonforst.evmap.viewmodel.getReferenceData
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
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
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastUpdateLocation: Location? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
|
||||
HashMap()
|
||||
private val maxRows = 6
|
||||
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val filterStatus = MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM } ?: 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)
|
||||
|
||||
init {
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address.city }.distinct().size > 1
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger, showCity))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (!favorites) {
|
||||
val filtersCount = filtersWithValue.value?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_filter
|
||||
)
|
||||
)
|
||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.pushForResult(FilterScreen(carContext)) {
|
||||
chargers = null
|
||||
numUpdates = 0
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
}
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
if (showCity && charger.address.city != null && charger.address.city !in charger.name) {
|
||||
setTitle(
|
||||
CarText.Builder("${charger.name} · ${charger.address.city}")
|
||||
.addVariant(charger.name)
|
||||
.build())
|
||||
} else {
|
||||
setTitle(charger.name)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distance = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
) / 1000
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(Distance.create(distance, Distance.UNIT_KILOMETERS)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${charger.maxPower.roundToInt()} kW")
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (lastUpdateLocation == null ||
|
||||
location.distanceTo(lastUpdateLocation) > updateThreshold
|
||||
) {
|
||||
lastUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
if (favorites) {
|
||||
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) > availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
chargers?.take(maxRows)?.map {
|
||||
lifecycleScope.async {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
val date = ZonedDateTime.now()
|
||||
val availability = getAvailability(it).data
|
||||
if (availability != null) {
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
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
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
.setTitle(carContext.getString(R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.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()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener {
|
||||
carContext.finishCarApp()
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
20
app/src/google/java/net/vonforst/evmap/auto/Utils.kt
Normal file
20
app/src/google/java/net/vonforst/evmap/auto/Utils.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.model.CarColor
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
|
||||
|
||||
return if (unknown) {
|
||||
CarColor.DEFAULT
|
||||
} else if (available > 0) {
|
||||
CarColor.GREEN
|
||||
} else if (allFaulted) {
|
||||
CarColor.RED
|
||||
} else {
|
||||
CarColor.BLUE
|
||||
}
|
||||
}
|
||||
70
app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
Normal file
70
app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.location.Location
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
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 {
|
||||
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
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,11 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.car2go.maps.google.adapter.AnyMapAdapter
|
||||
import com.google.android.libraries.places.api.model.Place
|
||||
import com.google.android.libraries.places.widget.Autocomplete
|
||||
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
|
||||
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
|
||||
import net.vonforst.evmap.viewmodel.PlaceWithBounds
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun launchAutocomplete(fragment: Fragment) {
|
||||
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
|
||||
val intent: Intent = Autocomplete.IntentBuilder(
|
||||
AutocompleteActivityMode.OVERLAY, fields
|
||||
)
|
||||
.build(fragment.requireActivity())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
// show keyboard
|
||||
val imm = fragment.requireContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
|
||||
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
|
||||
val place = Autocomplete.getPlaceFromIntent(intent)
|
||||
return PlaceWithBounds(AnyMapAdapter.adapt(place.latLng), AnyMapAdapter.adapt(place.viewport))
|
||||
}
|
||||
fun getAutocompleteProviders(context: Context) =
|
||||
if (PreferenceDataSource(context).searchProvider == "google") {
|
||||
listOf(GooglePlacesAutocompleteProvider(context), MapboxAutocompleteProvider(context))
|
||||
} else {
|
||||
listOf(MapboxAutocompleteProvider(context), GooglePlacesAutocompleteProvider(context))
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.CharacterStyle
|
||||
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.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
|
||||
import com.google.android.libraries.places.api.model.RectangularBounds
|
||||
import com.google.android.libraries.places.api.net.FetchPlaceRequest
|
||||
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest
|
||||
import com.google.android.libraries.places.api.net.PlacesStatusCodes
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import net.vonforst.evmap.R
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
||||
class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
private var token = AutocompleteSessionToken.newInstance()
|
||||
private val client = Places.createClient(context)
|
||||
private val bold: CharacterStyle = StyleSpan(Typeface.BOLD)
|
||||
|
||||
override fun autocomplete(
|
||||
query: String,
|
||||
location: com.car2go.maps.model.LatLng?
|
||||
): List<AutocompletePlace> {
|
||||
val request = FindAutocompletePredictionsRequest.builder().apply {
|
||||
if (location != null) {
|
||||
setLocationBias(calcLocationBias(location))
|
||||
setOrigin(LatLng(location.latitude, location.longitude))
|
||||
}
|
||||
setSessionToken(token)
|
||||
setQuery(query)
|
||||
}.build()
|
||||
try {
|
||||
val result =
|
||||
await(client.findAutocompletePredictions(request)).autocompletePredictions
|
||||
return result.map {
|
||||
AutocompletePlace(
|
||||
it.getPrimaryText(bold),
|
||||
it.getSecondaryText(bold),
|
||||
it.placeId,
|
||||
it.distanceMeters,
|
||||
it.placeTypes.map { AutocompletePlaceType.valueOf(it.name) })
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
val cause = e.cause
|
||||
if (cause is ApiException) {
|
||||
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
|
||||
throw ApiUnavailableException()
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
val request =
|
||||
FetchPlaceRequest.builder(id, listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)).build()
|
||||
try {
|
||||
val place = client.fetchPlace(request).await().place
|
||||
token = AutocompleteSessionToken.newInstance()
|
||||
return PlaceWithBounds(
|
||||
AnyMapAdapter.adapt(place.latLng),
|
||||
AnyMapAdapter.adapt(place.viewport)
|
||||
)
|
||||
} catch (e: ApiException) {
|
||||
if (e.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
|
||||
throw ApiUnavailableException()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAttributionString(): Int = R.string.places_powered_by_google
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
|
||||
|
||||
private fun calcLocationBias(location: com.car2go.maps.model.LatLng): RectangularBounds {
|
||||
val radius = 100e3 // meters
|
||||
val northEast =
|
||||
SphericalUtil.computeOffset(
|
||||
location,
|
||||
radius * sqrt(2.0),
|
||||
45.0
|
||||
)
|
||||
val southWest =
|
||||
SphericalUtil.computeOffset(
|
||||
location,
|
||||
radius * sqrt(2.0),
|
||||
225.0
|
||||
)
|
||||
return RectangularBounds.newInstance(
|
||||
LatLngBounds(
|
||||
AnyMapAdapter.adapt(southWest),
|
||||
AnyMapAdapter.adapt(northEast)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -28,7 +27,7 @@ class DonateFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_donate, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
@@ -36,14 +35,7 @@ class DonateFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.productsList.apply {
|
||||
adapter = DonationAdapter().apply {
|
||||
@@ -54,6 +46,10 @@ class DonateFragment : Fragment() {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
|
||||
vm.products.observe(viewLifecycleOwner) {
|
||||
print(it)
|
||||
}
|
||||
|
||||
vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer {
|
||||
Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
@@ -61,4 +57,12 @@ class DonateFragment : Fragment() {
|
||||
Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentOnboardingAndroidAutoBinding
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 4
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> AndroidAutoFragment()
|
||||
3 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidAutoFragment : OnboardingPageFragment() {
|
||||
private lateinit var binding: FragmentOnboardingAndroidAutoBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingAndroidAutoBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
parent.goToNext()
|
||||
}
|
||||
binding.imgAndroidAuto.alpha = 0f
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val animators =
|
||||
listOf(
|
||||
ObjectAnimator.ofFloat(binding.imgAndroidAuto, "translationY", -20f, 0f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
},
|
||||
ObjectAnimator.ofFloat(binding.imgAndroidAuto, "alpha", 0f, 1f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
)
|
||||
AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.imgAndroidAuto.alpha = 0f
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
.build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
|
||||
products.value = Resource.success(details
|
||||
products.postValue(Resource.success(details
|
||||
.sortedBy { it.priceAmountMicros }
|
||||
.map { DonationItem(it) }
|
||||
)
|
||||
))
|
||||
} else {
|
||||
products.value = Resource.error(result.debugMessage, null)
|
||||
products.postValue(Resource.error(result.debugMessage, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
android:id="@+id/products_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:data="@{vm.products.data}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_android_auto"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_android_auto_detail"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/sounds_cool"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_android_auto"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_marginBottom="28dp"
|
||||
android:background="@drawable/circle_bg_logo"
|
||||
android:backgroundTint="@color/android_auto_accent"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeTitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.7"
|
||||
app:srcCompat="@drawable/android_auto" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -25,8 +25,9 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.sku.title}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -34,7 +35,7 @@
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Spende" />
|
||||
tools:text="Spende (extrem langer Beschreibungstext)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView21"
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||
<string name="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
|
||||
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>
|
||||
<string name="auto_no_favorites_found">Keine Favoriten gefunden</string>
|
||||
@@ -16,4 +20,7 @@
|
||||
<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="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>
|
||||
</resources>
|
||||
@@ -8,8 +8,17 @@
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_search_provider_values" tranlatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_map_provider_default" translatable="false">google</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
|
||||
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
|
||||
<string name="auto_no_chargers_found">No nearby chargers found</string>
|
||||
<string name="auto_no_favorites_found">No favorites found</string>
|
||||
@@ -21,4 +30,7 @@
|
||||
<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="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>
|
||||
</resources>
|
||||
321
app/src/main/java/android/widget/Filter.java
Normal file
321
app/src/main/java/android/widget/Filter.java
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.widget;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Copy of android.widget.Filter, exposing the hidden setDelayer() method.
|
||||
*
|
||||
* <p>A filter constrains data with a filtering pattern.</p>
|
||||
*
|
||||
* <p>Filters are usually created by {@link android.widget.Filterable}
|
||||
* classes.</p>
|
||||
*
|
||||
* <p>Filtering operations performed by calling {@link #filter(CharSequence)} or
|
||||
* {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are
|
||||
* performed asynchronously. When these methods are called, a filtering request
|
||||
* is posted in a request queue and processed later. Any call to one of these
|
||||
* methods will cancel any previous non-executed filtering request.</p>
|
||||
*
|
||||
* @see android.widget.Filterable
|
||||
*/
|
||||
public abstract class Filter {
|
||||
private static final String LOG_TAG = "Filter";
|
||||
|
||||
private static final String THREAD_NAME = "Filter";
|
||||
private static final int FILTER_TOKEN = 0xD0D0F00D;
|
||||
private static final int FINISH_TOKEN = 0xDEADBEEF;
|
||||
|
||||
private Handler mThreadHandler;
|
||||
private Handler mResultHandler;
|
||||
|
||||
private Delayer mDelayer;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
|
||||
/**
|
||||
* <p>Creates a new asynchronous filter.</p>
|
||||
*/
|
||||
public Filter() {
|
||||
mResultHandler = new ResultsHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an interface that decides how long to delay the message for a given query. Useful
|
||||
* for heuristics such as posting a delay for the delete key to avoid doing any work while the
|
||||
* user holds down the delete key.
|
||||
*
|
||||
* @param delayer The delayer.
|
||||
* @hide
|
||||
*/
|
||||
public void setDelayer(Delayer delayer) {
|
||||
synchronized (mLock) {
|
||||
mDelayer = delayer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Starts an asynchronous filtering operation. Calling this method
|
||||
* cancels all previous non-executed filtering requests and posts a new
|
||||
* filtering request that will be executed later.</p>
|
||||
*
|
||||
* @param constraint the constraint used to filter the data
|
||||
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
||||
*/
|
||||
public final void filter(CharSequence constraint) {
|
||||
filter(constraint, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Starts an asynchronous filtering operation. Calling this method
|
||||
* cancels all previous non-executed filtering requests and posts a new
|
||||
* filtering request that will be executed later.</p>
|
||||
*
|
||||
* <p>Upon completion, the listener is notified.</p>
|
||||
*
|
||||
* @param constraint the constraint used to filter the data
|
||||
* @param listener a listener notified upon completion of the operation
|
||||
* @see #filter(CharSequence)
|
||||
* @see #performFiltering(CharSequence)
|
||||
* @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
|
||||
*/
|
||||
public final void filter(CharSequence constraint, FilterListener listener) {
|
||||
synchronized (mLock) {
|
||||
if (mThreadHandler == null) {
|
||||
HandlerThread thread = new HandlerThread(
|
||||
THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
mThreadHandler = new RequestHandler(thread.getLooper());
|
||||
}
|
||||
|
||||
final long delay = (mDelayer == null) ? 0 : mDelayer.getPostingDelay(constraint);
|
||||
|
||||
Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
|
||||
|
||||
RequestArguments args = new RequestArguments();
|
||||
// make sure we use an immutable copy of the constraint, so that
|
||||
// it doesn't change while the filter operation is in progress
|
||||
args.constraint = constraint != null ? constraint.toString() : null;
|
||||
args.listener = listener;
|
||||
message.obj = args;
|
||||
|
||||
mThreadHandler.removeMessages(FILTER_TOKEN);
|
||||
mThreadHandler.removeMessages(FINISH_TOKEN);
|
||||
mThreadHandler.sendMessageDelayed(message, delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Invoked in a worker thread to filter the data according to the
|
||||
* constraint. Subclasses must implement this method to perform the
|
||||
* filtering operation. Results computed by the filtering operation
|
||||
* must be returned as a {@link android.widget.Filter.FilterResults} that
|
||||
* will then be published in the UI thread through
|
||||
* {@link #publishResults(CharSequence,
|
||||
* android.widget.Filter.FilterResults)}.</p>
|
||||
*
|
||||
* <p><strong>Contract:</strong> When the constraint is null, the original
|
||||
* data must be restored.</p>
|
||||
*
|
||||
* @param constraint the constraint used to filter the data
|
||||
* @return the results of the filtering operation
|
||||
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
||||
* @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
|
||||
* @see android.widget.Filter.FilterResults
|
||||
*/
|
||||
protected abstract FilterResults performFiltering(CharSequence constraint);
|
||||
|
||||
/**
|
||||
* <p>Invoked in the UI thread to publish the filtering results in the
|
||||
* user interface. Subclasses must implement this method to display the
|
||||
* results computed in {@link #performFiltering}.</p>
|
||||
*
|
||||
* @param constraint the constraint used to filter the data
|
||||
* @param results the results of the filtering operation
|
||||
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
||||
* @see #performFiltering(CharSequence)
|
||||
* @see android.widget.Filter.FilterResults
|
||||
*/
|
||||
protected abstract void publishResults(CharSequence constraint,
|
||||
FilterResults results);
|
||||
|
||||
/**
|
||||
* <p>Converts a value from the filtered set into a CharSequence. Subclasses
|
||||
* should override this method to convert their results. The default
|
||||
* implementation returns an empty String for null values or the default
|
||||
* String representation of the value.</p>
|
||||
*
|
||||
* @param resultValue the value to convert to a CharSequence
|
||||
* @return a CharSequence representing the value
|
||||
*/
|
||||
public CharSequence convertResultToString(Object resultValue) {
|
||||
return resultValue == null ? "" : resultValue.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Holds the results of a filtering operation. The results are the values
|
||||
* computed by the filtering operation and the number of these values.</p>
|
||||
*/
|
||||
protected static class FilterResults {
|
||||
public FilterResults() {
|
||||
// nothing to see here
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Contains all the values computed by the filtering operation.</p>
|
||||
*/
|
||||
public Object values;
|
||||
|
||||
/**
|
||||
* <p>Contains the number of values computed by the filtering
|
||||
* operation.</p>
|
||||
*/
|
||||
public int count;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Listener used to receive a notification upon completion of a filtering
|
||||
* operation.</p>
|
||||
*/
|
||||
public static interface FilterListener {
|
||||
/**
|
||||
* <p>Notifies the end of a filtering operation.</p>
|
||||
*
|
||||
* @param count the number of values computed by the filter
|
||||
*/
|
||||
public void onFilterComplete(int count);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Worker thread handler. When a new filtering request is posted from
|
||||
* {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)},
|
||||
* it is sent to this handler.</p>
|
||||
*/
|
||||
private class RequestHandler extends Handler {
|
||||
public RequestHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Handles filtering requests by calling
|
||||
* {@link Filter#performFiltering} and then sending a message
|
||||
* with the results to the results handler.</p>
|
||||
*
|
||||
* @param msg the filtering request
|
||||
*/
|
||||
public void handleMessage(Message msg) {
|
||||
int what = msg.what;
|
||||
Message message;
|
||||
switch (what) {
|
||||
case FILTER_TOKEN:
|
||||
RequestArguments args = (RequestArguments) msg.obj;
|
||||
try {
|
||||
args.results = performFiltering(args.constraint);
|
||||
} catch (Exception e) {
|
||||
args.results = new FilterResults();
|
||||
Log.w(LOG_TAG, "An exception occured during performFiltering()!", e);
|
||||
} finally {
|
||||
message = mResultHandler.obtainMessage(what);
|
||||
message.obj = args;
|
||||
message.sendToTarget();
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
if (mThreadHandler != null) {
|
||||
Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
|
||||
mThreadHandler.sendMessageDelayed(finishMessage, 3000);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FINISH_TOKEN:
|
||||
synchronized (mLock) {
|
||||
if (mThreadHandler != null) {
|
||||
mThreadHandler.getLooper().quit();
|
||||
mThreadHandler = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Handles the results of a filtering operation. The results are
|
||||
* handled in the UI thread.</p>
|
||||
*/
|
||||
private class ResultsHandler extends Handler {
|
||||
/**
|
||||
* <p>Messages received from the request handler are processed in the
|
||||
* UI thread. The processing involves calling
|
||||
* {@link Filter#publishResults(CharSequence,
|
||||
* android.widget.Filter.FilterResults)}
|
||||
* to post the results back in the UI and then notifying the listener,
|
||||
* if any.</p>
|
||||
*
|
||||
* @param msg the filtering results
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
RequestArguments args = (RequestArguments) msg.obj;
|
||||
|
||||
publishResults(args.constraint, args.results);
|
||||
if (args.listener != null) {
|
||||
int count = args.results != null ? args.results.count : -1;
|
||||
args.listener.onFilterComplete(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Holds the arguments of a filtering request as well as the results
|
||||
* of the request.</p>
|
||||
*/
|
||||
private static class RequestArguments {
|
||||
/**
|
||||
* <p>The constraint used to filter the data.</p>
|
||||
*/
|
||||
CharSequence constraint;
|
||||
|
||||
/**
|
||||
* <p>The listener to notify upon completion. Can be null.</p>
|
||||
*/
|
||||
FilterListener listener;
|
||||
|
||||
/**
|
||||
* <p>The results of the filtering operation.</p>
|
||||
*/
|
||||
FilterResults results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public interface Delayer {
|
||||
|
||||
/**
|
||||
* @param constraint The constraint passed to {@link Filter#filter(CharSequence)}
|
||||
* @return The delay that should be used for
|
||||
* {@link Handler#sendMessageDelayed(android.os.Message, long)}
|
||||
*/
|
||||
long getPostingDelay(CharSequence constraint);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.limiter
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
|
||||
class EvMapApplication : Application() {
|
||||
override fun onCreate() {
|
||||
@@ -11,5 +16,28 @@ class EvMapApplication : Application() {
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
Stetho.initializeWithDefaults(this);
|
||||
init(applicationContext)
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.KEY_VALUE_LIST
|
||||
|
||||
mailSender {
|
||||
mailTo = "evmap+crashreport@vonforst.net"
|
||||
}
|
||||
|
||||
dialog {
|
||||
text = getString(R.string.crash_report_text)
|
||||
title = getString(R.string.app_name)
|
||||
commentPrompt = getString(R.string.crash_report_comment_prompt)
|
||||
resIcon = R.drawable.ic_launcher_foreground
|
||||
resTheme = R.style.AppTheme
|
||||
}
|
||||
|
||||
limiter {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ class MapsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
prefs = PreferenceDataSource(this)
|
||||
prefs.appStartCounter += 1
|
||||
|
||||
checkPlayServices(this)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
@@ -9,6 +11,7 @@ import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -80,6 +83,8 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> List<T>.containsAny(vararg values: T) = values.any { this.contains(it) }
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
return withContext(Dispatchers.Main.immediate) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
@@ -97,4 +102,14 @@ public suspend fun <T> LiveData<T>.await(): T {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isDarkMode() =
|
||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
const val kmPerMile = 1.609344
|
||||
const val meterPerFt = 0.3048
|
||||
|
||||
fun shouldUseImperialUnits(): Boolean {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean
|
||||
@@ -89,18 +88,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
}
|
||||
|
||||
class ChargepriceAdapter() :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
@@ -181,7 +168,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = position
|
||||
checkedItem = holder.bindingAdapterPosition
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.ItemFavoriteBinding
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> Unit) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun bind(
|
||||
holder: ViewHolder<FavoritesViewModel.FavoritesListItem>,
|
||||
item: FavoritesViewModel.FavoritesListItem
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFavoriteBinding
|
||||
binding.foreground.translationX = 0f
|
||||
binding.btnDelete.setOnClickListener {
|
||||
binding.foreground.animate()
|
||||
.translationX(binding.foreground.width.toFloat())
|
||||
.setDuration(250)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.withEndAction {
|
||||
onDelete(item)
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package net.vonforst.evmap.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
@@ -11,19 +13,11 @@ import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
|
||||
class GalleryAdapter(
|
||||
context: Context,
|
||||
val itemClickListener: ItemClickListener? = null,
|
||||
val detailView: Boolean = false,
|
||||
val pageToLoad: Int? = null,
|
||||
val imageCacheKey: MemoryCache.Key? = null,
|
||||
val loadedListener: (() -> Unit)? = null
|
||||
) :
|
||||
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
|
||||
ListAdapter<ChargerPhoto, GalleryAdapter.ViewHolder>(ChargerPhotoDiffCallback()) {
|
||||
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
@@ -38,104 +32,34 @@ class GalleryAdapter(
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: ImageView
|
||||
if (detailView) {
|
||||
view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView
|
||||
view.setOnTouchListener { v, event ->
|
||||
var result = true
|
||||
//can scroll horizontally checks if there's still a part of the image
|
||||
//that can be scrolled until you reach the edge
|
||||
if (event.pointerCount >= 2 || v.canScrollHorizontally(1) && v.canScrollHorizontally(
|
||||
-1
|
||||
)
|
||||
) {
|
||||
//multi-touch event
|
||||
result = when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
// Disallow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
// Disable touch on view
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Allow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
} else {
|
||||
view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
|
||||
}
|
||||
val view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
if (detailView) {
|
||||
(holder.view as TouchImageView).resetZoom()
|
||||
}
|
||||
val id = getItem(position).id
|
||||
val url = if (detailView) {
|
||||
getItem(position).getUrl(size = 1000)
|
||||
} else {
|
||||
getItem(position).getUrl(height = holder.view.height)
|
||||
}
|
||||
val url = getItem(position).getUrl(height = holder.view.height)
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
) {
|
||||
if (pageToLoad == position && imageCacheKey != null) {
|
||||
placeholderMemoryCacheKey(imageCacheKey)
|
||||
}
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
listener(
|
||||
onSuccess = { _, metadata ->
|
||||
memoryKeys[id] = metadata.memoryCacheKey
|
||||
if (pageToLoad == position) invokeLoadedListener(holder.view)
|
||||
},
|
||||
onError = { _, _ ->
|
||||
if (!loaded && loadedListener != null && pageToLoad == position) {
|
||||
loadedListener.invoke()
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (pageToLoad == position && imageCacheKey != null) {
|
||||
// start transition immediately
|
||||
if (pageToLoad == position) invokeLoadedListener(holder.view)
|
||||
}
|
||||
holder.view.transitionName = galleryTransitionName(position)
|
||||
|
||||
if (itemClickListener != null) {
|
||||
holder.view.setOnClickListener {
|
||||
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun invokeLoadedListener(
|
||||
view: ImageView
|
||||
) {
|
||||
if (!loaded && loadedListener != null) {
|
||||
view.viewTreeObserver.addOnPreDrawListener(object :
|
||||
ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
view.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
loadedListener.invoke()
|
||||
return true
|
||||
}
|
||||
})
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun galleryTransitionName(position: Int) = "gallery_$position"
|
||||
|
||||
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {
|
||||
override fun areItemsTheSame(oldItem: ChargerPhoto, newItem: ChargerPhoto): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.car2go.maps.model.LatLng
|
||||
import net.vonforst.evmap.R
|
||||
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.PreferenceDataSource
|
||||
|
||||
class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatLng>) :
|
||||
BaseAdapter(), Filterable {
|
||||
private var resultList: List<AutocompletePlace>? = null
|
||||
private val providers = getAutocompleteProviders(context)
|
||||
private val typeItem = 0
|
||||
private val typeAttribution = 1
|
||||
var currentProvider: AutocompleteProvider? = null
|
||||
|
||||
data class ViewHolder(val binding: ItemAutocompleteResultBinding)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return resultList?.let { it.size + 1 } ?: 0
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): AutocompletePlace? {
|
||||
return if (position < resultList!!.size) resultList!![position] else null
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position < resultList!!.size) typeItem else typeAttribution
|
||||
}
|
||||
|
||||
override fun getViewTypeCount(): Int = 2
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
var view = convertView
|
||||
if (getItemViewType(position) == typeItem) {
|
||||
val viewHolder: ViewHolder
|
||||
if (view == null) {
|
||||
val binding: ItemAutocompleteResultBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.item_autocomplete_result,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
view = binding.root
|
||||
viewHolder = ViewHolder(binding)
|
||||
view.tag = viewHolder
|
||||
} else {
|
||||
viewHolder = view.tag as ViewHolder
|
||||
}
|
||||
val place = resultList!![position]
|
||||
bindView(viewHolder, place)
|
||||
} else if (getItemViewType(position) == typeAttribution) {
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_autocomplete_attribution, parent, false)
|
||||
}
|
||||
(view as ImageView).apply {
|
||||
setImageResource(currentProvider?.getAttributionImage(context.isDarkMode()) ?: 0)
|
||||
contentDescription = context.getString(currentProvider?.getAttributionString() ?: 0)
|
||||
}
|
||||
|
||||
}
|
||||
return view!!
|
||||
}
|
||||
|
||||
private fun bindView(
|
||||
viewHolder: ViewHolder,
|
||||
place: AutocompletePlace
|
||||
) {
|
||||
viewHolder.binding.item = place
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
var delaySet = false
|
||||
|
||||
init {
|
||||
if (PreferenceDataSource(context).searchProvider == "mapbox") {
|
||||
// set delay to 500 ms to reduce paid Mapbox API requests
|
||||
this.setDelayer { 500L }
|
||||
}
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||
if (results != null && results.count > 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
if (constraint != null) {
|
||||
for (provider in providers) {
|
||||
try {
|
||||
resultList =
|
||||
provider.autocomplete(constraint.toString(), location.value)
|
||||
currentProvider = provider
|
||||
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 }
|
||||
}
|
||||
|
||||
return filterResults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun iconForPlaceType(types: List<AutocompletePlaceType>): Int =
|
||||
when {
|
||||
types.containsAny(
|
||||
AutocompletePlaceType.LIGHT_RAIL_STATION,
|
||||
AutocompletePlaceType.BUS_STATION,
|
||||
AutocompletePlaceType.TRAIN_STATION,
|
||||
AutocompletePlaceType.TRANSIT_STATION
|
||||
) -> {
|
||||
R.drawable.ic_place_type_train
|
||||
}
|
||||
types.contains(AutocompletePlaceType.AIRPORT) -> {
|
||||
R.drawable.ic_place_type_airport
|
||||
}
|
||||
// TODO: extend this with icons for more place categories
|
||||
else -> {
|
||||
R.drawable.ic_place_type_default
|
||||
}
|
||||
}
|
||||
|
||||
fun isSpecialPlace(types: List<AutocompletePlaceType>): Boolean =
|
||||
iconForPlaceType(types) != R.drawable.ic_place_type_default
|
||||
@@ -8,7 +8,8 @@ import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -117,17 +118,10 @@ data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String
|
||||
) {
|
||||
fun applyFilters(filters: FilterValues?): ChargeLocationStatus {
|
||||
if (filters == null) return this
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
val statusFiltered = status.filterKeys {
|
||||
(connectorsVal == null || connectorsVal.all || connectorsVal.values.map {
|
||||
equivalentPlugTypes(
|
||||
it
|
||||
)
|
||||
(connectors == null || connectors.map {
|
||||
equivalentPlugTypes(it)
|
||||
}.any { equivalent -> it.type in equivalent })
|
||||
&& (minPower == null || it.power > minPower)
|
||||
}
|
||||
|
||||
@@ -74,5 +74,51 @@ interface ChargepriceApi {
|
||||
.build()
|
||||
return retrofit.create(ChargepriceApi::class.java)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
|
||||
// list of countries updated 2021/08/24
|
||||
"goingelectric" -> country in listOf(
|
||||
"Deutschland",
|
||||
"Österreich",
|
||||
"Schweiz",
|
||||
"Frankreich",
|
||||
"Belgien",
|
||||
"Niederlande",
|
||||
"Luxemburg",
|
||||
"Dänemark",
|
||||
"Norwegen",
|
||||
"Schweden",
|
||||
"Slowenien",
|
||||
"Kroatien",
|
||||
"Ungarn",
|
||||
"Tschechien",
|
||||
"Italien",
|
||||
"Spanien",
|
||||
"Großbritannien",
|
||||
"Irland"
|
||||
)
|
||||
"openchargemap" -> country in listOf(
|
||||
"DE",
|
||||
"AT",
|
||||
"CH",
|
||||
"FR",
|
||||
"BE",
|
||||
"NE",
|
||||
"LU",
|
||||
"DK",
|
||||
"NO",
|
||||
"SE",
|
||||
"SI",
|
||||
"HR",
|
||||
"HU",
|
||||
"CZ",
|
||||
"IT",
|
||||
"ES",
|
||||
"GB",
|
||||
"IE"
|
||||
)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
@@ -142,15 +143,10 @@ class GoingElectricApiWrapper(
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null) {
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
connectorsVal.values = connectorsVal.values.mapNotNull {
|
||||
GEChargepoint.convertTypeToGE(it)
|
||||
}.toMutableSet()
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
@@ -241,15 +237,10 @@ class GoingElectricApiWrapper(
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null) {
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
connectorsVal.values = connectorsVal.values.mapNotNull {
|
||||
GEChargepoint.convertTypeToGE(it)
|
||||
}.toMutableSet()
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
@@ -354,40 +345,50 @@ class GoingElectricApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
val response = api.getChargepointDetail(id)
|
||||
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
|
||||
Resource.success(
|
||||
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
|
||||
apikey
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
|
||||
Resource.success(
|
||||
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
|
||||
apikey
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(response.message(), null)
|
||||
} else {
|
||||
Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<GEReferenceData> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
supervisorScope {
|
||||
try {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ data class GEChargeLocation(
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String) = ChargeLocation(
|
||||
id,
|
||||
"goingelectric",
|
||||
name,
|
||||
coordinates.convert(),
|
||||
address.convert(),
|
||||
|
||||
@@ -20,6 +20,7 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
|
||||
interface OpenChargeMapApi {
|
||||
@GET("poi/")
|
||||
@@ -137,29 +138,33 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
val operators = formatMultipleChoice(operatorsVal)
|
||||
|
||||
val response = api.getChargepoints(
|
||||
OCMBoundingBox(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude
|
||||
),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
OCMBoundingBox(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude
|
||||
),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
var result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
var result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
@@ -189,27 +194,31 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
val operators = formatMultipleChoice(operatorsVal)
|
||||
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude, location.longitude,
|
||||
radius.toDouble(),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
try {
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude, location.longitude,
|
||||
radius.toDouble(),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
val result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postprocessResult(
|
||||
@@ -244,20 +253,28 @@ class OpenChargeMapApiWrapper(
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val response = api.getChargepointDetail(id)
|
||||
if (response.isSuccessful) {
|
||||
return Resource.success(response.body()!![0].convert(referenceData))
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
if (response.isSuccessful && response.body()?.size == 1) {
|
||||
return Resource.success(response.body()!![0].convert(referenceData))
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<OCMReferenceData> {
|
||||
val response = api.getReferenceData()
|
||||
if (response.isSuccessful) {
|
||||
return Resource.success(response.body()!!)
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
try {
|
||||
val response = api.getReferenceData()
|
||||
if (response.isSuccessful) {
|
||||
return Resource.success(response.body()!!)
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ data class OCMChargepoint(
|
||||
) {
|
||||
fun convert(refData: OCMReferenceData) = ChargeLocation(
|
||||
id,
|
||||
"openchargemap",
|
||||
addressInfo.title,
|
||||
Coordinate(addressInfo.latitude, addressInfo.longitude),
|
||||
addressInfo.toAddress(refData),
|
||||
@@ -77,7 +78,7 @@ data class OCMChargepoint(
|
||||
val comment = userComments.filter { it.commentTypeId == faultReportCommentType }
|
||||
.maxByOrNull { it.dateCreated }
|
||||
if (comment != null) {
|
||||
return FaultReport(comment.dateCreated.toInstant(), comment.comment)
|
||||
return FaultReport(comment.dateCreated.toInstant(), comment.comment ?: "")
|
||||
}
|
||||
}
|
||||
if (statusType != null && statusType.id in faultStatuses) {
|
||||
@@ -229,7 +230,7 @@ data class OCMMediaItem(
|
||||
data class OCMUserComment(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "CommentTypeID") val commentTypeId: Long,
|
||||
@Json(name = "Comment") val comment: String,
|
||||
@Json(name = "Comment") val comment: String?,
|
||||
@Json(name = "UserName") val userName: String,
|
||||
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
|
||||
)
|
||||
@@ -257,4 +258,4 @@ class OCMChargerPhotoAdapter(
|
||||
else -> largeUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
|
||||
interface AutocompleteProvider {
|
||||
fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace>
|
||||
suspend fun getDetails(id: String): PlaceWithBounds
|
||||
|
||||
@StringRes
|
||||
fun getAttributionString(): Int
|
||||
|
||||
@DrawableRes
|
||||
fun getAttributionImage(dark: Boolean): Int
|
||||
}
|
||||
|
||||
data class AutocompletePlace(
|
||||
val primaryText: CharSequence,
|
||||
val secondaryText: CharSequence,
|
||||
val id: String,
|
||||
val distanceMeters: Int?,
|
||||
val types: List<AutocompletePlaceType>
|
||||
)
|
||||
|
||||
class ApiUnavailableException : Exception()
|
||||
|
||||
enum class AutocompletePlaceType {
|
||||
// based on Google Places Place.Type enum
|
||||
OTHER,
|
||||
ACCOUNTING,
|
||||
ADMINISTRATIVE_AREA_LEVEL_1,
|
||||
ADMINISTRATIVE_AREA_LEVEL_2,
|
||||
ADMINISTRATIVE_AREA_LEVEL_3,
|
||||
ADMINISTRATIVE_AREA_LEVEL_4,
|
||||
ADMINISTRATIVE_AREA_LEVEL_5,
|
||||
AIRPORT,
|
||||
AMUSEMENT_PARK,
|
||||
AQUARIUM,
|
||||
ARCHIPELAGO,
|
||||
ART_GALLERY,
|
||||
ATM,
|
||||
BAKERY,
|
||||
BANK,
|
||||
BAR,
|
||||
BEAUTY_SALON,
|
||||
BICYCLE_STORE,
|
||||
BOOK_STORE,
|
||||
BOWLING_ALLEY,
|
||||
BUS_STATION,
|
||||
CAFE,
|
||||
CAMPGROUND,
|
||||
CAR_DEALER,
|
||||
CAR_RENTAL,
|
||||
CAR_REPAIR,
|
||||
CAR_WASH,
|
||||
CASINO,
|
||||
CEMETERY,
|
||||
CHURCH,
|
||||
CITY_HALL,
|
||||
CLOTHING_STORE,
|
||||
COLLOQUIAL_AREA,
|
||||
CONTINENT,
|
||||
CONVENIENCE_STORE,
|
||||
COUNTRY,
|
||||
COURTHOUSE,
|
||||
DENTIST,
|
||||
DEPARTMENT_STORE,
|
||||
DOCTOR,
|
||||
DRUGSTORE,
|
||||
ELECTRICIAN,
|
||||
ELECTRONICS_STORE,
|
||||
EMBASSY,
|
||||
ESTABLISHMENT,
|
||||
FINANCE,
|
||||
FIRE_STATION,
|
||||
FLOOR,
|
||||
FLORIST,
|
||||
FOOD,
|
||||
FUNERAL_HOME,
|
||||
FURNITURE_STORE,
|
||||
GAS_STATION,
|
||||
GENERAL_CONTRACTOR,
|
||||
GEOCODE,
|
||||
GROCERY_OR_SUPERMARKET,
|
||||
GYM,
|
||||
HAIR_CARE,
|
||||
HARDWARE_STORE,
|
||||
HEALTH,
|
||||
HINDU_TEMPLE,
|
||||
HOME_GOODS_STORE,
|
||||
HOSPITAL,
|
||||
INSURANCE_AGENCY,
|
||||
INTERSECTION,
|
||||
JEWELRY_STORE,
|
||||
LAUNDRY,
|
||||
LAWYER,
|
||||
LIBRARY,
|
||||
LIGHT_RAIL_STATION,
|
||||
LIQUOR_STORE,
|
||||
LOCAL_GOVERNMENT_OFFICE,
|
||||
LOCALITY,
|
||||
LOCKSMITH,
|
||||
LODGING,
|
||||
MEAL_DELIVERY,
|
||||
MEAL_TAKEAWAY,
|
||||
MOSQUE,
|
||||
MOVIE_RENTAL,
|
||||
MOVIE_THEATER,
|
||||
MOVING_COMPANY,
|
||||
MUSEUM,
|
||||
NATURAL_FEATURE,
|
||||
NEIGHBORHOOD,
|
||||
NIGHT_CLUB,
|
||||
PAINTER,
|
||||
PARK,
|
||||
PARKING,
|
||||
PET_STORE,
|
||||
PHARMACY,
|
||||
PHYSIOTHERAPIST,
|
||||
PLACE_OF_WORSHIP,
|
||||
PLUMBER,
|
||||
PLUS_CODE,
|
||||
POINT_OF_INTEREST,
|
||||
POLICE,
|
||||
POLITICAL,
|
||||
POST_BOX,
|
||||
POST_OFFICE,
|
||||
POSTAL_CODE_PREFIX,
|
||||
POSTAL_CODE_SUFFIX,
|
||||
POSTAL_CODE,
|
||||
POSTAL_TOWN,
|
||||
PREMISE,
|
||||
PRIMARY_SCHOOL,
|
||||
REAL_ESTATE_AGENCY,
|
||||
RESTAURANT,
|
||||
ROOFING_CONTRACTOR,
|
||||
ROOM,
|
||||
ROUTE,
|
||||
RV_PARK,
|
||||
SCHOOL,
|
||||
SECONDARY_SCHOOL,
|
||||
SHOE_STORE,
|
||||
SHOPPING_MALL,
|
||||
SPA,
|
||||
STADIUM,
|
||||
STORAGE,
|
||||
STORE,
|
||||
STREET_ADDRESS,
|
||||
STREET_NUMBER,
|
||||
SUBLOCALITY_LEVEL_1,
|
||||
SUBLOCALITY_LEVEL_2,
|
||||
SUBLOCALITY_LEVEL_3,
|
||||
SUBLOCALITY_LEVEL_4,
|
||||
SUBLOCALITY_LEVEL_5,
|
||||
SUBLOCALITY,
|
||||
SUBPREMISE,
|
||||
SUBWAY_STATION,
|
||||
SUPERMARKET,
|
||||
SYNAGOGUE,
|
||||
TAXI_STAND,
|
||||
TOURIST_ATTRACTION,
|
||||
TOWN_SQUARE,
|
||||
TRAIN_STATION,
|
||||
TRANSIT_STATION,
|
||||
TRAVEL_AGENCY,
|
||||
UNIVERSITY,
|
||||
VETERINARY_CARE,
|
||||
ZOO;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrNull(value: String): AutocompletePlaceType? {
|
||||
try {
|
||||
return valueOf(value)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
|
||||
@@ -0,0 +1,125 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.car2go.maps.util.SphericalUtil
|
||||
import com.mapbox.api.geocoding.v5.GeocodingCriteria
|
||||
import com.mapbox.api.geocoding.v5.MapboxGeocoding
|
||||
import com.mapbox.api.geocoding.v5.models.CarmenFeature
|
||||
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 fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace> {
|
||||
val result = MapboxGeocoding.builder().apply {
|
||||
location?.let {
|
||||
proximity(Point.fromLngLat(location.longitude, location.latitude))
|
||||
}
|
||||
languages(ConfigurationCompat.getLocales(context.resources.configuration)[0].language)
|
||||
accessToken(context.getString(R.string.mapbox_key))
|
||||
autocomplete(true)
|
||||
this.query(query)
|
||||
}.build().executeCall()
|
||||
if (!result.isSuccessful) {
|
||||
throw IOException(result.message())
|
||||
}
|
||||
return result.body()!!.features().map { feature ->
|
||||
results[feature.id()!!] = feature
|
||||
var secondaryText = (feature.matchingPlaceName() ?: feature.placeName())!!
|
||||
|
||||
val matchingText = (feature.matchingText() ?: feature.text())!!
|
||||
val primaryText =
|
||||
if (feature.address() != null && secondaryText.startsWith(feature.address() + " " + matchingText)) {
|
||||
// countries where house number comes in front of road ("10 Downing Street")
|
||||
feature.address() + " " + matchingText
|
||||
} else {
|
||||
// countries where house number comes after road ("Willy-Brandt-Str. 1")
|
||||
matchingText + (feature.address()?.let { " $it" } ?: "")
|
||||
}
|
||||
|
||||
secondaryText = secondaryText.replace("$primaryText, ", "")
|
||||
AutocompletePlace(
|
||||
highlightMatch(primaryText, query),
|
||||
secondaryText,
|
||||
feature.id()!!,
|
||||
location?.let { location ->
|
||||
SphericalUtil.computeDistanceBetween(
|
||||
feature.center()!!.toLatLng(), location
|
||||
).roundToInt()
|
||||
},
|
||||
getPlaceTypes(feature)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPlaceTypes(feature: CarmenFeature): List<AutocompletePlaceType> {
|
||||
val types = feature.placeType()?.mapNotNull {
|
||||
when (it) {
|
||||
GeocodingCriteria.TYPE_COUNTRY -> AutocompletePlaceType.COUNTRY
|
||||
GeocodingCriteria.TYPE_REGION -> AutocompletePlaceType.ADMINISTRATIVE_AREA_LEVEL_1
|
||||
GeocodingCriteria.TYPE_POSTCODE -> AutocompletePlaceType.POSTAL_CODE
|
||||
GeocodingCriteria.TYPE_DISTRICT -> AutocompletePlaceType.ADMINISTRATIVE_AREA_LEVEL_2
|
||||
GeocodingCriteria.TYPE_PLACE -> AutocompletePlaceType.ADMINISTRATIVE_AREA_LEVEL_3
|
||||
GeocodingCriteria.TYPE_LOCALITY -> AutocompletePlaceType.LOCALITY
|
||||
GeocodingCriteria.TYPE_NEIGHBORHOOD -> AutocompletePlaceType.NEIGHBORHOOD
|
||||
GeocodingCriteria.TYPE_ADDRESS -> AutocompletePlaceType.STREET_ADDRESS
|
||||
GeocodingCriteria.TYPE_POI -> AutocompletePlaceType.POINT_OF_INTEREST
|
||||
GeocodingCriteria.TYPE_POI_LANDMARK -> AutocompletePlaceType.POINT_OF_INTEREST
|
||||
else -> null
|
||||
}
|
||||
} ?: emptyList()
|
||||
val categories = feature.properties()?.get("category")?.asString?.split(", ")?.mapNotNull {
|
||||
// Place categories are defined at https://docs.mapbox.com/api/search/geocoding/#point-of-interest-category-coverage
|
||||
// We try to find a matching entry in the enum.
|
||||
// TODO: map categories that are not named the same
|
||||
AutocompletePlaceType.valueOfOrNull(it.uppercase().replace(" ", "_"))
|
||||
} ?: emptyList()
|
||||
return types + categories
|
||||
}
|
||||
|
||||
private fun highlightMatch(text: String, query: String): CharSequence {
|
||||
val result = SpannableString(text)
|
||||
|
||||
val startPos = text.lowercase().indexOf(query.lowercase())
|
||||
if (startPos > -1) {
|
||||
val endPos = startPos + query.length
|
||||
result.setSpan(bold, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
val place = results[id]!!
|
||||
results.clear()
|
||||
return PlaceWithBounds(
|
||||
place.center()!!.toLatLng(),
|
||||
place.geometry()?.bbox()?.toLatLngBounds()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAttributionString(): Int = R.string.powered_by_mapbox
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int = R.drawable.mapbox_logo_icon
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
return LatLngBounds(
|
||||
southwest().toLatLng(),
|
||||
northeast().toLatLng()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Point.toLatLng(): LatLng = LatLng(this.latitude(), this.longitude())
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
@@ -14,13 +13,12 @@ import net.vonforst.evmap.R
|
||||
|
||||
|
||||
class AboutFragment : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
@@ -59,6 +57,10 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
findNavController().navigate(R.id.action_about_to_donateFragment)
|
||||
true
|
||||
}
|
||||
"github_sponsors" -> {
|
||||
findNavController().navigate(R.id.action_about_to_github_sponsors)
|
||||
true
|
||||
}
|
||||
"twitter" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url))
|
||||
true
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -85,14 +84,6 @@ class ChargepriceFragment : DialogFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val charger = requireArguments().getParcelable<ChargeLocation>(ARG_CHARGER)!!
|
||||
val dataSource = requireArguments().getString(ARG_DATASOURCE)!!
|
||||
vm.charger.value = charger
|
||||
@@ -161,11 +152,11 @@ class ChargepriceFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
binding.imgChargepriceLogo.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
|
||||
(requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${dataSource}")
|
||||
}
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
navController.navigate(R.id.action_chargeprice_to_settingsFragment)
|
||||
findNavController().navigate(R.id.action_chargeprice_to_settingsFragment)
|
||||
}
|
||||
|
||||
binding.batteryRange.setLabelFormatter { value: Float ->
|
||||
@@ -217,6 +208,14 @@ class ChargepriceFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_CHARGER = "charger"
|
||||
const val ARG_DATASOURCE = "datasource"
|
||||
|
||||
@@ -41,9 +41,10 @@ class DataSourceSelectDialog : AppCompatDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// dialog with 95% screen height
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
(resources.displayMetrics.heightPixels * 0.95).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ 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
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -14,20 +16,29 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
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.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 toDelete: ChargeLocation? = null
|
||||
private var deleteSnackbar: Snackbar? = null
|
||||
private lateinit var adapter: FavoritesAdapter
|
||||
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -58,21 +69,19 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val favAdapter = FavoritesAdapter(vm).apply {
|
||||
adapter = FavoritesAdapter(onDelete = {
|
||||
delete(it.charger)
|
||||
}).apply {
|
||||
onClickListener = {
|
||||
navController.navigate(R.id.action_favs_to_map, MapFragment.showCharger(it.charger))
|
||||
findNavController().navigate(
|
||||
R.id.action_favs_to_map,
|
||||
MapFragment.showCharger(it.charger)
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.favsList.apply {
|
||||
adapter = favAdapter
|
||||
adapter = this@FavoritesFragment.adapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
@@ -81,6 +90,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
)
|
||||
)
|
||||
}
|
||||
createTouchHelper().attachToRecyclerView(binding.favsList)
|
||||
|
||||
locationClient.connect()
|
||||
}
|
||||
@@ -109,4 +119,144 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
locationClient.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(fav: ChargeLocation) {
|
||||
val position = vm.listData.value?.indexOfFirst { it.charger == fav } ?: return
|
||||
// if there is already a profile to delete, delete it now
|
||||
actuallyDelete()
|
||||
deleteSnackbar?.dismiss()
|
||||
|
||||
toDelete = fav
|
||||
|
||||
view?.let {
|
||||
val snackbar = Snackbar.make(
|
||||
it,
|
||||
getString(R.string.deleted_filterprofile, fav.name),
|
||||
Snackbar.LENGTH_LONG
|
||||
).setAction(R.string.undo) {
|
||||
toDelete = null
|
||||
adapter.notifyItemChanged(position)
|
||||
}.addCallback(object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
// if undo was not clicked, actually delete
|
||||
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
})
|
||||
deleteSnackbar = snackbar
|
||||
snackbar.show()
|
||||
} ?: run {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun actuallyDelete() {
|
||||
toDelete?.let { vm.deleteFavorite(it) }
|
||||
toDelete = null
|
||||
}
|
||||
|
||||
private fun createTouchHelper(): ItemTouchHelper {
|
||||
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
0,
|
||||
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
|
||||
) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val fav = vm.favorites.value?.find { it.id == viewHolder.itemId }
|
||||
fav?.let { delete(it) }
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
if (viewHolder != null && actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
val binding =
|
||||
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
|
||||
getDefaultUIUtil().onSelected(binding.foreground)
|
||||
} else {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChildDrawOver(
|
||||
c: Canvas, recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
|
||||
actionState: Int, isCurrentlyActive: Boolean
|
||||
) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
val binding =
|
||||
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
|
||||
getDefaultUIUtil().onDrawOver(
|
||||
c, recyclerView, binding.foreground, dX, dY,
|
||||
actionState, isCurrentlyActive
|
||||
)
|
||||
val lp = (binding.deleteIcon.layoutParams as FrameLayout.LayoutParams)
|
||||
lp.gravity = Gravity.CENTER_VERTICAL or if (dX > 0) {
|
||||
Gravity.START
|
||||
} else {
|
||||
Gravity.END
|
||||
}
|
||||
binding.deleteIcon.layoutParams = lp
|
||||
} else {
|
||||
super.onChildDrawOver(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
) {
|
||||
val binding =
|
||||
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
|
||||
getDefaultUIUtil().clearView(binding.foreground)
|
||||
}
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas, recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
|
||||
actionState: Int, isCurrentlyActive: Boolean
|
||||
) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
val binding =
|
||||
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFavoriteBinding
|
||||
getDefaultUIUtil().onDraw(
|
||||
c, recyclerView, binding.foreground, dX, dY,
|
||||
actionState, isCurrentlyActive
|
||||
)
|
||||
} else {
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package net.vonforst.evmap.fragment
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -42,14 +41,13 @@ class FilterFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
binding.toolbar.title = "${getString(R.string.menu_filter)}: ${it.name}"
|
||||
}
|
||||
}
|
||||
|
||||
binding.filtersList.apply {
|
||||
adapter = FiltersAdapter()
|
||||
@@ -62,7 +60,7 @@ class FilterFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
@@ -104,4 +102,12 @@ class FilterFragment : Fragment() {
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -57,15 +56,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
|
||||
|
||||
touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
@@ -207,11 +198,19 @@ class FilterProfilesFragment : Fragment() {
|
||||
|
||||
touchHelper.attachToRecyclerView(binding.filterProfilesList)
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
fun delete(fp: FilterProfile) {
|
||||
val position = vm.filterProfiles.value?.indexOf(fp) ?: return
|
||||
// if there is already a profile to delete, delete it now
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import coil.memory.MemoryCache
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentGalleryBinding
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
|
||||
|
||||
class GalleryFragment : Fragment() {
|
||||
companion object {
|
||||
private const val EXTRA_POSITION = "position"
|
||||
private const val EXTRA_PHOTOS = "photos"
|
||||
private const val EXTRA_IMAGE_CACHE_KEY = "image_cache_key"
|
||||
private const val SAVED_CURRENT_PAGE_POSITION = "current_page_position"
|
||||
|
||||
fun buildArgs(
|
||||
photos: List<ChargerPhoto>,
|
||||
position: Int,
|
||||
imageCacheKey: MemoryCache.Key?
|
||||
): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelableArrayList(EXTRA_PHOTOS, ArrayList(photos))
|
||||
putInt(EXTRA_POSITION, position)
|
||||
putParcelable(EXTRA_IMAGE_CACHE_KEY, imageCacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentGalleryBinding
|
||||
private var startingPosition: Int = 0
|
||||
private var currentPosition: Int = 0
|
||||
private lateinit var galleryAdapter: GalleryAdapter
|
||||
private var currentPage: TouchImageView? = null
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
|
||||
private val backPressedCallback = object :
|
||||
OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val image = currentPage
|
||||
if (image != null && image.currentZoom !in 0.95f..1.05f) {
|
||||
image.setZoomAnimated(1f, 0.5f, 0.5f)
|
||||
} else {
|
||||
galleryVm.galleryPosition.value = currentPosition
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_gallery, container, false
|
||||
)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val args = requireArguments()
|
||||
startingPosition = args.getInt(EXTRA_POSITION, 0)
|
||||
currentPosition =
|
||||
savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition
|
||||
|
||||
galleryAdapter =
|
||||
GalleryAdapter(
|
||||
requireContext(), detailView = true, pageToLoad = currentPosition,
|
||||
imageCacheKey = args.getParcelable(EXTRA_IMAGE_CACHE_KEY)
|
||||
) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
binding.gallery.setPageTransformer { page, _ ->
|
||||
val v = page as TouchImageView
|
||||
currentPage = v
|
||||
}
|
||||
binding.gallery.adapter = galleryAdapter
|
||||
binding.photos = args.getParcelableArrayList(EXTRA_PHOTOS)
|
||||
|
||||
binding.gallery.post {
|
||||
binding.gallery.setCurrentItem(currentPosition, false)
|
||||
binding.gallery.registerOnPageChangeCallback(object :
|
||||
ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
currentPosition = position
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(R.transition.image_shared_element_transition)
|
||||
sharedElementReturnTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(R.transition.image_shared_element_transition)
|
||||
setEnterSharedElementCallback(enterElementCallback)
|
||||
if (savedInstanceState == null) {
|
||||
postponeEnterTransition();
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
backPressedCallback
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(SAVED_CURRENT_PAGE_POSITION, currentPosition)
|
||||
}
|
||||
|
||||
private val enterElementCallback: SharedElementCallback = object : SharedElementCallback() {
|
||||
override fun onMapSharedElements(
|
||||
names: MutableList<String>,
|
||||
sharedElements: MutableMap<String, View>
|
||||
) {
|
||||
val currentPage = currentPage ?: return
|
||||
sharedElements[names[0]] = currentPage
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,16 +2,18 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.method.KeyListener
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
@@ -19,10 +21,8 @@ import androidx.annotation.RequiresPermission
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.*
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -31,14 +31,16 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.MapFragment
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
@@ -57,6 +59,7 @@ import com.mapzen.android.lost.api.LocationListener
|
||||
import com.mapzen.android.lost.api.LocationRequest
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -66,9 +69,10 @@ import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.autocomplete.handleAutocompleteResult
|
||||
import net.vonforst.evmap.autocomplete.launchAutocomplete
|
||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
@@ -79,9 +83,9 @@ import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.boundingBox
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
const val REQUEST_AUTOCOMPLETE = 2
|
||||
const val ARG_CHARGER_ID = "chargerId"
|
||||
const val ARG_LAT = "lat"
|
||||
const val ARG_LON = "lon"
|
||||
@@ -98,6 +102,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var requestingLocationUpdates = false
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
private var searchResultMarker: Marker? = null
|
||||
@@ -117,6 +122,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.search.hasFocus()) {
|
||||
removeSearchFocus()
|
||||
}
|
||||
|
||||
val state = bottomSheetBehavior.state
|
||||
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
@@ -131,6 +140,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this)
|
||||
.build()
|
||||
@@ -147,7 +158,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
val provider = PreferenceDataSource(requireContext()).mapProvider
|
||||
val provider = prefs.mapProvider
|
||||
if (mapFragment == null || mapFragment!!.priority[0] != provider) {
|
||||
mapFragment = MapFragment()
|
||||
mapFragment!!.priority = arrayOf(
|
||||
@@ -173,7 +184,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
postponeEnterTransition()
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -194,7 +204,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
insets
|
||||
}
|
||||
|
||||
setExitSharedElementCallback(reenterSharedElementCallback)
|
||||
exitTransition = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(R.transition.map_exit_transition)
|
||||
|
||||
@@ -216,11 +225,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailAppBar.toolbar.menu.findItem(R.id.menu_edit).title =
|
||||
getString(R.string.edit_at_datasource, vm.apiName)
|
||||
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
}
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
setupAdapters()
|
||||
(activity as? MapsActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
if (prefs.appStartCounter > 5 && !prefs.opensourceDonationsDialogShown) {
|
||||
try {
|
||||
findNavController().navigate(R.id.action_map_to_opensource_donations)
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
// when there is already another navigation going on
|
||||
}
|
||||
}
|
||||
/*if (!prefs.update060AndroidAutoDialogShown) {
|
||||
try {
|
||||
navController.navigate(R.id.action_map_to_update_060_androidauto)
|
||||
@@ -296,9 +316,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
||||
}
|
||||
binding.search.setOnClickListener {
|
||||
launchAutocomplete(this)
|
||||
}
|
||||
setupSearchAutocomplete()
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
}
|
||||
@@ -335,6 +353,68 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
var searchKeyListener: KeyListener? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setupSearchAutocomplete() {
|
||||
binding.search.threshold = 1
|
||||
|
||||
searchKeyListener = binding.search.keyListener
|
||||
binding.search.keyListener = null
|
||||
|
||||
val adapter = PlaceAutocompleteAdapter(requireContext(), vm.location)
|
||||
binding.search.setAdapter(adapter)
|
||||
binding.search.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
||||
val place = adapter.getItem(position) ?: return@OnItemClickListener
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
vm.searchResult.value = adapter.currentProvider!!.getDetails(place.id)
|
||||
} catch (e: ApiUnavailableException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
// TODO: show error
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
removeSearchFocus()
|
||||
binding.search.setText(
|
||||
if (place.secondaryText.isNotEmpty()) {
|
||||
"${place.primaryText}, ${place.secondaryText}"
|
||||
} else {
|
||||
place.primaryText.toString()
|
||||
}
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
binding.clearSearch.setOnClickListener {
|
||||
vm.searchResult.value = null
|
||||
removeSearchFocus()
|
||||
}
|
||||
binding.toolbar.doOnLayout {
|
||||
binding.search.dropDownWidth = binding.toolbar.width
|
||||
binding.search.dropDownAnchor = R.id.toolbar
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSearchFocus() {
|
||||
// clear focus and hide keyboard
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(binding.search.windowToken, 0)
|
||||
binding.search.clearFocus()
|
||||
}
|
||||
|
||||
private fun openLayersMenu() {
|
||||
binding.fabLayers.tag = false
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
@@ -459,6 +539,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
} else {
|
||||
binding.search.setText("")
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
@@ -483,6 +565,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|
||||
|| vm.searchResult.value != null
|
||||
|| (vm.layersMenuOpen.value ?: false)
|
||||
|| binding.search.hasFocus()
|
||||
}
|
||||
|
||||
private fun unhighlightAllMarkers() {
|
||||
@@ -537,24 +620,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
private fun setupAdapters() {
|
||||
var viewer: StfalconImageViewer<ChargerPhoto>? = null
|
||||
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
|
||||
override fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?) {
|
||||
val photos = vm.charger.value?.data?.photos ?: return
|
||||
val extras = FragmentNavigatorExtras(view to view.transitionName)
|
||||
view.findNavController().navigate(
|
||||
R.id.action_map_to_galleryFragment,
|
||||
GalleryFragment.buildArgs(photos, position, imageCacheKey),
|
||||
null,
|
||||
extras
|
||||
)
|
||||
|
||||
viewer = StfalconImageViewer.Builder(context, photos) { imageView, photo ->
|
||||
imageView.load(photo.getUrl(size = 1000)) {
|
||||
if (photo == photos[position] && imageCacheKey != null) {
|
||||
placeholderMemoryCacheKey(imageCacheKey)
|
||||
}
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
.withTransitionFrom(view as ImageView)
|
||||
.withImageChangeListener {
|
||||
binding.gallery.layoutManager!!.scrollToPosition(it)
|
||||
binding.gallery.layoutManager!!.findViewByPosition(it)?.let {
|
||||
viewer?.updateTransitionImage(it as ImageView)
|
||||
}
|
||||
}
|
||||
.withStartPosition(position)
|
||||
.show()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val galleryPosition = galleryVm.galleryPosition.value
|
||||
binding.gallery.apply {
|
||||
adapter = GalleryAdapter(context, galleryClickListener, pageToLoad = galleryPosition) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
adapter = GalleryAdapter(context, galleryClickListener)
|
||||
itemAnimator = null
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
@@ -565,41 +659,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
setDrawable(ContextCompat.getDrawable(context, R.drawable.gallery_divider)!!)
|
||||
})
|
||||
}
|
||||
if (galleryPosition == null) {
|
||||
startPostponedEnterTransition()
|
||||
} else {
|
||||
binding.gallery.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
v: View,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int
|
||||
) {
|
||||
v.removeOnLayoutChangeListener(this)
|
||||
val layoutManager = binding.gallery.layoutManager!!
|
||||
val viewAtPosition = layoutManager.findViewByPosition(galleryPosition)
|
||||
if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(
|
||||
viewAtPosition,
|
||||
false,
|
||||
true
|
||||
)
|
||||
) {
|
||||
binding.gallery.post {
|
||||
layoutManager.scrollToPosition(galleryPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// make sure that the app does not freeze waiting for a picture to load
|
||||
Handler().postDelayed({
|
||||
startPostponedEnterTransition()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
binding.detailView.connectors.apply {
|
||||
adapter = ConnectorAdapter()
|
||||
@@ -1096,38 +1155,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_AUTOCOMPLETE -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
vm.searchResult.value = handleAutocompleteResult(data)
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRootView(): View {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private val reenterSharedElementCallback: SharedElementCallback =
|
||||
object : SharedElementCallback() {
|
||||
override fun onMapSharedElements(
|
||||
names: MutableList<String>,
|
||||
sharedElements: MutableMap<String, View>
|
||||
) {
|
||||
// Locate the ViewHolder for the clicked position.
|
||||
val position = galleryVm.galleryPosition.value ?: return
|
||||
|
||||
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
|
||||
if (vh?.itemView == null) return
|
||||
|
||||
// Map the first shared element name to the child ImageView.
|
||||
sharedElements[names[0]] = vh.itemView
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun showCharger(charger: ChargeLocation): Bundle {
|
||||
return Bundle().apply {
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.*
|
||||
@@ -20,6 +19,7 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
class OnboardingFragment : Fragment() {
|
||||
private lateinit var binding: FragmentOnboardingBinding
|
||||
private lateinit var adapter: OnboardingViewPagerAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -28,7 +28,7 @@ class OnboardingFragment : Fragment() {
|
||||
): View {
|
||||
binding = FragmentOnboardingBinding.inflate(inflater)
|
||||
|
||||
val adapter = OnboardingViewPagerAdapter(this)
|
||||
adapter = OnboardingViewPagerAdapter(this)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.pageIndicatorView.count = adapter.itemCount
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
@@ -57,7 +57,7 @@ class OnboardingFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun goToNext() {
|
||||
if (binding.viewPager.currentItem == 2) {
|
||||
if (binding.viewPager.currentItem == adapter.itemCount - 1) {
|
||||
findNavController().navigate(R.id.action_onboarding_to_map)
|
||||
} else {
|
||||
binding.viewPager.setCurrentItem(binding.viewPager.currentItem + 1, true)
|
||||
@@ -65,18 +65,6 @@ class OnboardingFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OnboardingPageFragment : Fragment() {
|
||||
lateinit var parent: OnboardingFragment
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.fragment
|
||||
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
|
||||
@@ -36,15 +37,8 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
vm.vehicles.observe(viewLifecycleOwner) { res ->
|
||||
@@ -64,7 +58,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
res.data?.let { tariffs ->
|
||||
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
|
||||
myTariffsPreference.entries = tariffs.map {
|
||||
if (!it.name.startsWith(it.provider)) {
|
||||
if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
|
||||
"${it.provider} ${it.name}"
|
||||
} else {
|
||||
it.name
|
||||
@@ -77,13 +71,18 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
|
||||
private fun updateMyTariffsSummary() {
|
||||
myTariffsPreference.summary = if (prefs.chargepriceMyTariffsAll) {
|
||||
getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
requireContext().resources
|
||||
.getQuantityString(R.plurals.chargeprice_some_tariffs_selected, n, n)
|
||||
}
|
||||
myTariffsPreference.summary =
|
||||
if (prefs.chargepriceMyTariffsAll) {
|
||||
getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
requireContext().resources
|
||||
.getQuantityString(
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + getString(R.string.pref_my_tariffs_summary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyVehiclesSummary() {
|
||||
@@ -124,12 +123,25 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
"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() {
|
||||
|
||||
@@ -6,28 +6,39 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import net.vonforst.evmap.databinding.DialogUpdate060AndroidautoBinding
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.DialogOpensourceDonationsBinding
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
class Update060AndroidAutoDialogFramgent : AppCompatDialogFragment() {
|
||||
private lateinit var binding: DialogUpdate060AndroidautoBinding
|
||||
class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
|
||||
private lateinit var binding: DialogOpensourceDonationsBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DialogUpdate060AndroidautoBinding.inflate(inflater, container, false)
|
||||
binding = DialogOpensourceDonationsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
binding.btnOk.setOnClickListener {
|
||||
PreferenceDataSource(requireContext()).update060AndroidAutoDialogShown = true
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
dismiss()
|
||||
}
|
||||
binding.btnDonate.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
findNavController().navigate(R.id.action_opensource_donations_to_donate)
|
||||
}
|
||||
binding.btnGithubSponsors.setOnClickListener {
|
||||
prefs.opensourceDonationsDialogShown = true
|
||||
findNavController().navigate(R.id.action_opensource_donations_to_github_sponsors)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -24,10 +24,11 @@ import kotlin.math.floor
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@Entity
|
||||
@Entity(primaryKeys = ["id", "dataSource"])
|
||||
@Parcelize
|
||||
data class ChargeLocation(
|
||||
@PrimaryKey val id: Long,
|
||||
val id: Long,
|
||||
val dataSource: String,
|
||||
val name: String,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
|
||||
@@ -48,6 +48,8 @@ sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var dataSource: String = ""
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
|
||||
abstract fun hasSameValueAs(other: FilterValue): Boolean
|
||||
}
|
||||
|
||||
@Entity(
|
||||
@@ -62,7 +64,11 @@ sealed class FilterValue : BaseObservable(), Equatable {
|
||||
data class BooleanFilterValue(
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
) : FilterValue() {
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is BooleanFilterValue && other.value == this.value
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
@@ -78,23 +84,14 @@ data class MultipleChoiceFilterValue(
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is MultipleChoiceFilterValue) return false
|
||||
if (key != other.key) return false
|
||||
|
||||
return if (all) {
|
||||
other.all
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is MultipleChoiceFilterValue && if (other.all) {
|
||||
this.all
|
||||
} else {
|
||||
!other.all && values == other.values
|
||||
!this.all && other.values == this.values
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = key.hashCode()
|
||||
result = 31 * result + all.hashCode()
|
||||
result = 31 * result + if (all) 0 else values.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
@@ -109,7 +106,11 @@ data class MultipleChoiceFilterValue(
|
||||
data class SliderFilterValue(
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
) : FilterValue() {
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is SliderFilterValue && other.value == this.value
|
||||
}
|
||||
}
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
@Navigator.Name("chrome")
|
||||
class ChromeCustomTabsNavigator(
|
||||
@Navigator.Name("custom")
|
||||
class CustomNavigator(
|
||||
private val context: Context
|
||||
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
|
||||
) : Navigator<CustomNavigator.Destination>() {
|
||||
|
||||
override fun createDestination() =
|
||||
Destination(this)
|
||||
@@ -28,6 +29,22 @@ class ChromeCustomTabsNavigator(
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?
|
||||
): NavDestination? {
|
||||
if (destination.destination == "report_new_charger") {
|
||||
val prefs = PreferenceDataSource(context)
|
||||
val url = when (prefs.dataSource) {
|
||||
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
|
||||
"openchargemap" -> "https://openchargemap.org/site/poi/add"
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
launchCustomTab(url)
|
||||
}
|
||||
if (destination.destination == "github_sponsors") {
|
||||
launchCustomTab(context.getString(R.string.github_sponsors_link))
|
||||
}
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
}
|
||||
|
||||
fun launchCustomTab(url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -35,20 +52,19 @@ class ChromeCustomTabsNavigator(
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(context, destination.url!!)
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
intent.launchUrl(context, Uri.parse(url))
|
||||
}
|
||||
|
||||
override fun popBackStack() = true // Managed by Chrome Custom Tabs
|
||||
|
||||
@NavDestination.ClassType(Activity::class)
|
||||
class Destination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
|
||||
var url: Uri? = null
|
||||
lateinit var destination: String
|
||||
|
||||
override fun onInflate(context: Context, attrs: AttributeSet) {
|
||||
super.onInflate(context, attrs)
|
||||
context.withStyledAttributes(attrs, R.styleable.ChromeCustomTabsNavigator, 0, 0) {
|
||||
url = Uri.parse(getString(R.styleable.ChromeCustomTabsNavigator_url))
|
||||
context.withStyledAttributes(attrs, R.styleable.CustomNavigator, 0, 0) {
|
||||
destination = getString(R.styleable.CustomNavigator_customDestination)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ class NavHostFragment : NavHostFragment() {
|
||||
override fun onCreateNavController(navController: NavController) {
|
||||
super.onCreateNavController(navController)
|
||||
navController.navigatorProvider.addNavigator(
|
||||
ChromeCustomTabsNavigator(
|
||||
CustomNavigator(
|
||||
requireContext()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,9 @@ interface ChargeLocationsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertBlocking(vararg locations: ChargeLocation)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg locations: ChargeLocation)
|
||||
|
||||
@@ -17,4 +20,7 @@ interface ChargeLocationsDao {
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
@@ -8,6 +10,7 @@ import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargepoint
|
||||
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
|
||||
import net.vonforst.evmap.api.openchargemap.OCMCountry
|
||||
import net.vonforst.evmap.api.openchargemap.OCMOperator
|
||||
@@ -26,7 +29,7 @@ import net.vonforst.evmap.model.*
|
||||
OCMConnectionType::class,
|
||||
OCMCountry::class,
|
||||
OCMOperator::class
|
||||
], version = 12
|
||||
], version = 13
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -47,7 +50,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_12, MIGRATION_13
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
@@ -251,5 +254,52 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_13 = object : Migration(12, 13) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
// add column dataSource to ChargeLocation table
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `dataSource` TEXT NOT NULL DEFAULT 'openchargemap'")
|
||||
|
||||
// this should have been included in MIGRATION_12:
|
||||
// Update GoingElectric format of plug types for favorites to generic EVMap format
|
||||
val cursor = db.query("SELECT * FROM `ChargeLocation`")
|
||||
while (cursor.moveToNext()) {
|
||||
val chargepoints =
|
||||
Converters().toChargepointList(cursor.getString(cursor.getColumnIndex("chargepoints")))!!
|
||||
val updated = chargepoints.map {
|
||||
it.copy(type = GEChargepoint.convertTypeFromGE(it.type))
|
||||
}
|
||||
if (updated != chargepoints) {
|
||||
db.update(
|
||||
"ChargeLocation",
|
||||
SQLiteDatabase.CONFLICT_ROLLBACK,
|
||||
ContentValues().apply {
|
||||
put("chargepoints", Converters().fromChargepointList(updated))
|
||||
put("dataSource", "goingelectric")
|
||||
},
|
||||
"id = ?",
|
||||
arrayOf(cursor.getLong(cursor.getColumnIndex("id")))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// update ChargeLocation table to change primary key
|
||||
db.execSQL(
|
||||
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))"
|
||||
);
|
||||
val columnList =
|
||||
"`id`,`dataSource`,`name`,`chargepoints`,`network`,`url`,`editUrl`,`verified`,`barrierFree`,`operator`,`generalInformation`,`amenities`,`locationDescription`,`photos`,`chargecards`,`license`,`lat`,`lng`,`city`,`country`,`postcode`,`street`,`fault_report_created`,`fault_report_description`,`twentyfourSeven`,`description`,`mostart`,`moend`,`tustart`,`tuend`,`westart`,`weend`,`thstart`,`thend`,`frstart`,`frend`,`sastart`,`saend`,`sustart`,`suend`,`hostart`,`hoend`,`freecharging`,`freeparking`,`descriptionShort`,`descriptionLong`,`chargepricecountry`,`chargepricenetwork`,`chargepriceplugTypes`"
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew`($columnList) SELECT $columnList FROM `ChargeLocation`")
|
||||
db.execSQL("DROP TABLE `ChargeLocation`")
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,10 @@ abstract class FilterValueDao {
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
value = sources.mapNotNull { it.value }.flatten()
|
||||
val values = sources.map { it.value }
|
||||
if (values.all { it != null }) {
|
||||
value = values.filterNotNull().flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,12 @@ class PreferenceDataSource(val context: Context) {
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
|
||||
val searchProvider: String
|
||||
get() = sp.getString(
|
||||
"search_provider",
|
||||
context.getString(R.string.pref_search_provider_default)
|
||||
)!!
|
||||
|
||||
var mapType: AnyMap.Type
|
||||
get() = AnyMap.Type.valueOf(sp.getString("map_type", null) ?: AnyMap.Type.NORMAL.toString())
|
||||
set(type) {
|
||||
@@ -105,7 +111,12 @@ class PreferenceDataSource(val context: Context) {
|
||||
}
|
||||
|
||||
var chargepriceMyVehicles: Set<String>
|
||||
get() = sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
get() = try {
|
||||
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
} catch (e: ClassCastException) {
|
||||
// backwards compatibility
|
||||
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
|
||||
}
|
||||
set(value) {
|
||||
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
|
||||
}
|
||||
@@ -156,4 +167,17 @@ class PreferenceDataSource(val context: Context) {
|
||||
.putFloat("chargeprice_battery_range_max", value[1])
|
||||
.apply()
|
||||
}
|
||||
|
||||
/** App start counter, introduced with Version 1.0.0 */
|
||||
var appStartCounter: Long
|
||||
get() = sp.getLong("app_start_counter", 0)
|
||||
set(value) {
|
||||
sp.edit().putLong("app_start_counter", value).apply()
|
||||
}
|
||||
|
||||
var opensourceDonationsDialogShown: Boolean
|
||||
get() = sp.getBoolean("opensource_donations_dialog_shown", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("opensource_donations_dialog_shown", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.text.HtmlCompat
|
||||
@@ -22,6 +23,11 @@ import com.google.android.material.slider.RangeSlider
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.kmPerMile
|
||||
import net.vonforst.evmap.meterPerFt
|
||||
import net.vonforst.evmap.shouldUseImperialUnits
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@@ -75,16 +81,34 @@ fun invisibleUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
|
||||
|
||||
@BindingAdapter("isFabActive")
|
||||
fun isFabActive(view: FloatingActionButton, isColored: Boolean) {
|
||||
val color = view.context.theme.obtainStyledAttributes(
|
||||
view.imageTintList = activeTint(view.context, isColored)
|
||||
}
|
||||
|
||||
@BindingAdapter("backgroundTintActive")
|
||||
fun backgroundTintActive(view: View, isColored: Boolean) {
|
||||
view.backgroundTintList = activeTint(view.context, isColored)
|
||||
}
|
||||
|
||||
@BindingAdapter("imageTintActive")
|
||||
fun imageTintActive(view: ImageView, isColored: Boolean) {
|
||||
view.imageTintList = activeTint(view.context, isColored)
|
||||
}
|
||||
|
||||
private fun activeTint(
|
||||
context: Context,
|
||||
isColored: Boolean
|
||||
): ColorStateList {
|
||||
val color = context.theme.obtainStyledAttributes(
|
||||
intArrayOf(
|
||||
if (isColored) {
|
||||
R.attr.colorAccent
|
||||
R.attr.colorPrimary
|
||||
} else {
|
||||
R.attr.colorControlNormal
|
||||
}
|
||||
)
|
||||
)
|
||||
view.imageTintList = ColorStateList.valueOf(color.getColor(0, 0))
|
||||
val valueOf = ColorStateList.valueOf(color.getColor(0, 0))
|
||||
return valueOf
|
||||
}
|
||||
|
||||
@BindingAdapter("data")
|
||||
@@ -265,6 +289,33 @@ fun currency(currency: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt();
|
||||
val min = ceil(value.toDouble() % 60).toInt();
|
||||
return if (h == 0 && min > 0) "$min min";
|
||||
else "%d:%02d h".format(h, min);
|
||||
}
|
||||
|
||||
fun distance(meters: Number?): String? {
|
||||
if (meters == null) return null
|
||||
if (shouldUseImperialUnits()) {
|
||||
val ft = meters.toDouble() / meterPerFt
|
||||
val mi = meters.toDouble() / 1e3 / kmPerMile
|
||||
return when {
|
||||
ft < 1000 -> "%.0f ft".format(ft)
|
||||
mi < 10 -> "%.1f mi".format(mi)
|
||||
else -> "%.0f mi".format(mi)
|
||||
}
|
||||
} else {
|
||||
val km = meters.toDouble() / 1e3
|
||||
return when {
|
||||
km < 1 -> "%.0f m".format(meters.toDouble())
|
||||
km < 10 -> "%.1f km".format(km)
|
||||
else -> "%.0f km".format(km)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "app:values")
|
||||
fun getRangeSliderValue(slider: RangeSlider) = slider.values
|
||||
|
||||
@@ -302,4 +353,9 @@ fun myTariffsBackground(view: View, myTariff: Boolean) {
|
||||
view.background = it.getDrawable(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("tooltipTextCompat")
|
||||
fun setTooltipTextCompat(view: View, text: String) {
|
||||
TooltipCompat.setTooltipText(view, text)
|
||||
}
|
||||
@@ -92,7 +92,14 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
MutableLiveData<List<Float>>().apply {
|
||||
value = prefs.chargepriceBatteryRange
|
||||
observeForever {
|
||||
prefs.chargepriceBatteryRange = it
|
||||
if (it[0] == it[1]) {
|
||||
value = if (it[0] < 1.0) {
|
||||
listOf(it[0], it[1] + 1)
|
||||
} else {
|
||||
listOf(it[0] - 1, it[1])
|
||||
}
|
||||
}
|
||||
prefs.chargepriceBatteryRange = value!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +172,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
|
||||
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
|
||||
val index = charger.value!!.chargepoints.indexOf(chargepoint)
|
||||
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
|
||||
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
|
||||
return type
|
||||
}
|
||||
@@ -209,6 +216,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
private var loadPricesJob: Job? = null
|
||||
fun loadPrices() {
|
||||
chargePrices.value = Resource.loading(null)
|
||||
chargePriceMeta.value = Resource.loading(null)
|
||||
val charger = charger.value
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
|
||||
81
app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt
Normal file
81
app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt
Normal file
@@ -0,0 +1,81 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
fun ChargepointApi<ReferenceData>.getReferenceData(
|
||||
scope: CoroutineScope,
|
||||
ctx: Context
|
||||
): LiveData<out ReferenceData> {
|
||||
val db = AppDatabase.getInstance(ctx)
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
return when (this) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
this,
|
||||
scope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
this,
|
||||
scope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<FilterValues> =
|
||||
MediatorLiveData<FilterValues>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val f = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = f.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ChargepointApi<ReferenceData>.getFilters(
|
||||
referenceData: LiveData<out ReferenceData>,
|
||||
stringProvider: StringProvider
|
||||
) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = getFilters(data, stringProvider)
|
||||
}
|
||||
}
|
||||
|
||||
fun FilterValueDao.getFilterValues(filterStatus: LiveData<Long>, dataSource: String) =
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = getFilterValues(status, dataSource)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
loc.longitude,
|
||||
charger.coordinates.lat,
|
||||
charger.coordinates.lng
|
||||
) / 1000
|
||||
)
|
||||
}
|
||||
})
|
||||
}?.sortedBy { it.distance }
|
||||
|
||||
@@ -5,60 +5,19 @@ import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.reflect.full.cast
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<FilterValues> =
|
||||
MediatorLiveData<FilterValues>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val f = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = f.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FilterViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
val api = api
|
||||
when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
private val referenceData = api.getReferenceData(viewModelScope, application)
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = api.getFilters(data, application.stringProvider())
|
||||
@@ -93,11 +52,13 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
suspend fun saveFilterValues() {
|
||||
filtersWithValue.value?.forEach {
|
||||
filtersWithValue.value?.map {
|
||||
val value = it.value
|
||||
value.profile = FILTERS_CUSTOM
|
||||
value.dataSource = prefs.dataSource
|
||||
db.filterValueDao().insert(value)
|
||||
value
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
@@ -113,11 +74,13 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
// save filter values
|
||||
filtersWithValue.value?.forEach {
|
||||
filtersWithValue.value?.map {
|
||||
val value = it.value
|
||||
value.profile = profileId
|
||||
value.dataSource = prefs.dataSource
|
||||
db.filterValueDao().insert(value)
|
||||
value
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
@@ -17,15 +18,16 @@ import net.vonforst.evmap.api.openchargemap.OCMConnection
|
||||
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
|
||||
|
||||
internal fun getClusterDistance(zoom: Float): Int? {
|
||||
return when (zoom) {
|
||||
in 0.0..7.0 -> 100
|
||||
@@ -53,48 +55,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val mapPosition: MutableLiveData<MapPosition> by lazy {
|
||||
MutableLiveData<MapPosition>()
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = db.filterValueDao().getFilterValues(status, prefs.dataSource)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
val filterStatus: MutableLiveData<Long> by lazy {
|
||||
MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus
|
||||
observeForever {
|
||||
prefs.filterStatus = it
|
||||
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
|
||||
}
|
||||
}
|
||||
}
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
val api = api
|
||||
when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
val api = api
|
||||
value = api.getFilters(data, application.stringProvider())
|
||||
}
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> =
|
||||
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
|
||||
private val referenceData = api.getReferenceData(viewModelScope, application)
|
||||
private val filters = api.getFilters(referenceData, application.stringProvider())
|
||||
|
||||
private val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
@@ -124,7 +97,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
value = 0
|
||||
addSource(filtersWithValue) { filtersWithValue ->
|
||||
value = filtersWithValue.count {
|
||||
it.filter.defaultValue() != it.value
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +116,9 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>()
|
||||
}
|
||||
val filteredMinPower: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
}
|
||||
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
|
||||
MutableLiveData<Set<Long>>()
|
||||
}
|
||||
@@ -188,7 +164,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
loc.longitude,
|
||||
charger.coordinates.lat,
|
||||
charger.coordinates.lng
|
||||
) / 1000
|
||||
)
|
||||
} else null
|
||||
}
|
||||
addSource(chargerSparse, callback)
|
||||
@@ -217,13 +193,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val av = availability.value
|
||||
val filters = filtersWithValue.value
|
||||
if (av?.status == Status.SUCCESS && filters != null) {
|
||||
value = Resource.success(av.data!!.applyFilters(filters))
|
||||
value = Resource.success(
|
||||
av.data!!.applyFilters(
|
||||
filteredConnectors.value,
|
||||
filteredMinPower.value
|
||||
)
|
||||
)
|
||||
} else {
|
||||
value = av
|
||||
}
|
||||
}
|
||||
addSource(availability, callback)
|
||||
addSource(filtersWithValue, callback)
|
||||
addSource(filteredConnectors, callback)
|
||||
addSource(filteredMinPower, callback)
|
||||
}
|
||||
}
|
||||
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
|
||||
@@ -261,16 +243,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
val filterStatus: MutableLiveData<Long> by lazy {
|
||||
MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus
|
||||
observeForever {
|
||||
prefs.filterStatus = it
|
||||
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPrefs() {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
@@ -287,9 +259,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
if (filterStatus.value == FILTERS_CUSTOM) return
|
||||
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
filterValues.value?.forEach {
|
||||
filterValues.value?.map {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
db.filterValueDao().insert(it)
|
||||
it
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +297,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
) { 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
|
||||
@@ -347,6 +322,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
GEChargepoint.convertTypeFromGE(it)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("minPower")
|
||||
} else if (api is OpenChargeMapApiWrapper) {
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
@@ -356,6 +332,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
refData as OCMReferenceData
|
||||
)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("minPower")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,9 +341,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
availability.value = getAvailability(charger)
|
||||
}
|
||||
|
||||
private var chargerLoadingTask: Job? = null
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
chargerLoadingTask?.cancel()
|
||||
chargerLoadingTask = viewModelScope.launch {
|
||||
try {
|
||||
chargerDetails.value = api.getChargepointDetail(referenceData, charger.id)
|
||||
} catch (e: IOException) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 158 KiB |
11
app/src/main/res/drawable/circle_bg_autocomplete.xml
Normal file
11
app/src/main/res/drawable/circle_bg_autocomplete.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#33FFFFFF" />
|
||||
<size
|
||||
android:height="24dp"
|
||||
android:width="24dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
10
app/src/main/res/drawable/ic_place_type_airport.xml
Normal file
10
app/src/main/res/drawable/ic_place_type_airport.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22,16v-2l-8.5,-5V3.5C13.5,2.67 12.83,2 12,2s-1.5,0.67 -1.5,1.5V9L2,14v2l8.5,-2.5V19L8,20.5L8,22l4,-1l4,1l0,-1.5L13.5,19v-5.5L22,16z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_place_type_default.xml
Normal file
10
app/src/main/res/drawable/ic_place_type_default.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_place_type_train.xml
Normal file
10
app/src/main/res/drawable/ic_place_type_train.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2c-4,0 -8,0.5 -8,4v9.5C4,17.43 5.57,19 7.5,19L6,20.5v0.5h2.23l2,-2L14,19l2,2h2v-0.5L16.5,19c1.93,0 3.5,-1.57 3.5,-3.5L20,6c0,-3.5 -3.58,-4 -8,-4zM7.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5S6.67,14 7.5,14s1.5,0.67 1.5,1.5S8.33,17 7.5,17zM11,10L6,10L6,6h5v4zM13,10L13,6h5v4h-5zM16.5,17c-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.5z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
<item android:drawable="?selectableItemBackground" />
|
||||
</layer-list>
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceApi" />
|
||||
|
||||
<variable
|
||||
name="charger"
|
||||
type="Resource<ChargeLocation>" />
|
||||
@@ -112,9 +114,9 @@
|
||||
android:gravity="end"
|
||||
android:maxLines="1"
|
||||
android:minWidth="50dp"
|
||||
android:text="@{@string/distance_format(distance)}"
|
||||
android:text="@{BindingAdaptersKt.distance(distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/topPart"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
tools:text="10 km" />
|
||||
|
||||
@@ -302,6 +304,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:text="TextView"
|
||||
android:layout_marginBottom="-10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
@@ -314,6 +317,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
|
||||
app:icon="@drawable/ic_chargeprice"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
@@ -327,7 +331,7 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:contentDescription="@string/verified"
|
||||
android:tooltipText="@{@string/verified_desc(apiName)}"
|
||||
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
|
||||
app:goneUnless="@{ charger.data.verified }"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtName"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
|
||||
@@ -344,7 +348,7 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:contentDescription="@string/fault_report"
|
||||
android:tooltipText="@string/fault_report"
|
||||
app:tooltipTextCompat="@{@string/fault_report}"
|
||||
app:goneUnless="@{ charger.data.faultReport != null }"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtName"
|
||||
app:layout_constraintStart_toEndOf="@+id/txtName"
|
||||
|
||||
@@ -6,30 +6,57 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialogTitle"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/pref_data_source"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dataSourceDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
<ScrollView
|
||||
android:id="@+id/scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialogTitle" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialogTitle"
|
||||
app:layout_constraintBottom_toTopOf="@id/btnCancel">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dataSourceDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOK"
|
||||
@@ -40,9 +67,8 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ok"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rg_data_source" />
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
@@ -52,20 +78,6 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOK"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rg_data_source" />
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dataSourceDescription" />
|
||||
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
126
app/src/main/res/layout/dialog_opensource_donations.xml
Normal file
126
app/src/main/res/layout/dialog_opensource_donations.xml
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:maxWidth="200dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/topPanel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="88dp"
|
||||
android:background="@color/colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/circle_bg_logo"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/heart_anim" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/donation_dialog_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topPanel" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/donation_dialog_detail"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGithubSponsors"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:text="@string/github_sponsors"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/btnDonate"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnDonate"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDonate"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/btnOk"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOk"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnGithubSponsors" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOk"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ok"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnDonate" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/flow"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="btnGithubSponsors,btnDonate,btnOk"
|
||||
app:flow_horizontalBias="1"
|
||||
app:flow_wrapMode="chain"
|
||||
app:flow_horizontalStyle="packed"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:maxWidth="200dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/topPanel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="88dp"
|
||||
android:background="@color/android_auto_accent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView4"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/circle_bg_logo"
|
||||
android:scaleType="center"
|
||||
app:srcCompat="@drawable/android_auto" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/update_060_androidauto_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topPanel" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/update_060_androidauto_text"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
|
||||
app:srcCompat="@drawable/android_auto_screenshot" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOk"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ok" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
@@ -99,6 +100,21 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{@string/chargeprice_duration(BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)))}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="(25 min)" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.model.ChargerPhoto" />
|
||||
|
||||
<import type="java.util.List" />
|
||||
|
||||
<variable
|
||||
name="photos"
|
||||
type="List<ChargerPhoto>" />
|
||||
</data>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/gallery"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
app:data="@{photos}" />
|
||||
</layout>
|
||||
@@ -58,16 +58,47 @@
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp">
|
||||
android:layout_height="48dp"
|
||||
app:contentInsetStartWithNavigation="70dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/search"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/search"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:textColor="?android:textColorSecondary" />
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:singleLine="true"
|
||||
android:scrollHorizontally="true"
|
||||
android:ellipsize="end"
|
||||
android:background="@null"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/search"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:dropDownVerticalOffset="8dp"
|
||||
android:popupBackground="@drawable/rounded_rect_4dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clearSearch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:invisibleUnless="@{search.text.length() > 0}"
|
||||
app:tint="?colorControlNormal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/handle"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
android:contentDescription="@string/delete" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/rl_create_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
|
||||
@@ -1,63 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/pref_data_source"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rg_data_source"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/pref_data_source"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/lets_go"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rg_data_source"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/lets_go"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
@@ -8,11 +8,13 @@
|
||||
android:id="@+id/icon1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iconLabel1"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel1"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.7"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:srcCompat="@drawable/ic_map_marker_charging"
|
||||
app:tint="@color/charger_low"
|
||||
@@ -26,7 +28,7 @@
|
||||
android:layout_marginBottom="28dp"
|
||||
android:text="< 11 kW"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/iconLabel2"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -134,7 +136,7 @@
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_2_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
@@ -145,13 +147,30 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_2_detail"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_2"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
@@ -6,14 +6,21 @@
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="28dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeTitle"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_max="256dp"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.85"
|
||||
app:layout_constraintWidth_max="256dp"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/logo_anim"
|
||||
app:lottie_speed="0.75" />
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.ortiz.touchview.TouchImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
11
app/src/main/res/layout/item_autocomplete_attribution.xml
Normal file
11
app/src/main/res/layout/item_autocomplete_attribution.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="18dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:scaleType="fitStart"
|
||||
tools:src="@drawable/places_powered_by_google_light" />
|
||||
91
app/src/main/res/layout/item_autocomplete_result.xml
Normal file
91
app/src/main/res/layout/item_autocomplete_result.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.text.util.Linkify" />
|
||||
<import type="net.vonforst.evmap.adapter.PlaceAutocompleteAdapterKt" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.autocomplete.AutocompletePlace" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@{item.primaryText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@{item.secondaryText}"
|
||||
android:padding="6dp"
|
||||
android:background="@drawable/circle_bg_autocomplete"
|
||||
android:backgroundTintMode="src_in"
|
||||
app:imageTintActive="@{PlaceAutocompleteAdapterKt.isSpecialPlace(item.types)}"
|
||||
app:backgroundTintActive="@{PlaceAutocompleteAdapterKt.isSpecialPlace(item.types)}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@{PlaceAutocompleteAdapterKt.iconForPlaceType(item.types)}"
|
||||
tools:srcCompat="@drawable/ic_address"
|
||||
tools:tint="?colorControlNormal"
|
||||
tools:backgroundTint="?colorControlNormal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@{BindingAdaptersKt.distance(item.distanceMeters)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.distanceMeters != null}"
|
||||
app:layout_constraintEnd_toEndOf="@+id/icon"
|
||||
app:layout_constraintStart_toStartOf="@+id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/icon"
|
||||
tools:text="9999 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@{item.secondaryText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -60,7 +60,7 @@
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@{item.provider}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{!item.tariffName.startsWith(item.provider)}"
|
||||
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rvTags"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.UtilsKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
|
||||
|
||||
<variable
|
||||
@@ -15,21 +18,45 @@
|
||||
type="net.vonforst.evmap.viewmodel.FavoritesViewModel.FavoritesListItem" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="wrap_content"
|
||||
<FrameLayout
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/delete_red"> <!--Add your background color here-->
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
app:tint="@android:color/white"
|
||||
app:srcCompat="@drawable/ic_delete" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/foreground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.charger.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/selectable_opaque_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.charger.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
@@ -59,40 +86,57 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="2x Typ 2 22 kW" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:goneUnless="@{item.distance != null}"
|
||||
android:text="@{@string/distance_format(item.distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9999,9 km" />
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{BindingAdaptersKt.distance(item.distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.distance != null}"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9999,9 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
app:goneUnless="@{item.available.status == Status.SUCCESS}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
app:goneUnless="@{item.available.status == Status.SUCCESS}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar4"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:goneUnless="@{item.available.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar4"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{item.available.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:tint="?colorControlNormal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_delete"
|
||||
android:contentDescription="@string/delete" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
</layout>
|
||||
@@ -9,13 +9,6 @@
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label="MapFragment"
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_galleryFragment"
|
||||
app:destination="@id/gallery"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
@@ -40,6 +33,9 @@
|
||||
<action
|
||||
android:id="@+id/action_map_to_update_060_androidauto"
|
||||
app:destination="@id/update_060_androidauto" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/about"
|
||||
@@ -49,17 +45,15 @@
|
||||
<action
|
||||
android:id="@+id/action_about_to_donateFragment"
|
||||
app:destination="@id/donate" />
|
||||
<action
|
||||
android:id="@+id/action_about_to_github_sponsors"
|
||||
app:destination="@id/github_sponsors" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings"
|
||||
android:name="net.vonforst.evmap.fragment.SettingsFragment"
|
||||
android:label="@string/settings"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/gallery"
|
||||
android:name="net.vonforst.evmap.fragment.GalleryFragment"
|
||||
android:label="GalleryFragment"
|
||||
tools:layout="@layout/fragment_gallery" />
|
||||
<fragment
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
@@ -102,9 +96,24 @@
|
||||
android:name="net.vonforst.evmap.fragment.updatedialogs.Update060AndroidAutoDialogFramgent"
|
||||
android:label="@string/welcome_to_evmap"
|
||||
tools:layout="@layout/dialog_update_060_androidauto" />
|
||||
<chrome
|
||||
<dialog
|
||||
android:id="@+id/opensource_donations"
|
||||
android:name="net.vonforst.evmap.fragment.updatedialogs.OpensourceDonationsDialogFramgent"
|
||||
android:label="@string/donation_dialog_title"
|
||||
tools:layout="@layout/dialog_opensource_donations">
|
||||
<action
|
||||
android:id="@+id/action_opensource_donations_to_donate"
|
||||
app:destination="@id/donate" />
|
||||
<action
|
||||
android:id="@+id/action_opensource_donations_to_github_sponsors"
|
||||
app:destination="@id/github_sponsors" />
|
||||
</dialog>
|
||||
<custom
|
||||
android:id="@+id/report_new_charger"
|
||||
app:url="@string/report_new_charger_url" />
|
||||
app:customDestination="report_new_charger" />
|
||||
<custom
|
||||
android:id="@+id/github_sponsors"
|
||||
app:customDestination="github_sponsors" />
|
||||
<fragment
|
||||
android:id="@+id/onboarding"
|
||||
android:name="net.vonforst.evmap.fragment.OnboardingFragment"
|
||||
|
||||
593
app/src/main/res/raw/heart_anim.json
Normal file
593
app/src/main/res/raw/heart_anim.json
Normal file
@@ -0,0 +1,593 @@
|
||||
{
|
||||
"v": "5.7.11",
|
||||
"fr": 60,
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"w": 56,
|
||||
"h": 56,
|
||||
"nm": "favorite_black_24dp",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "Formebene 1",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [
|
||||
0.833
|
||||
],
|
||||
"y": [
|
||||
0.833
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"x": [
|
||||
0.167
|
||||
],
|
||||
"y": [
|
||||
0.167
|
||||
]
|
||||
},
|
||||
"t": 13,
|
||||
"s": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": {
|
||||
"x": [
|
||||
0.833
|
||||
],
|
||||
"y": [
|
||||
0.833
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"x": [
|
||||
0.167
|
||||
],
|
||||
"y": [
|
||||
0.167
|
||||
]
|
||||
},
|
||||
"t": 15,
|
||||
"s": [
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 26,
|
||||
"s": [
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
28,
|
||||
26,
|
||||
0
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [
|
||||
0.369,
|
||||
0.369,
|
||||
0.667
|
||||
],
|
||||
"y": [
|
||||
1.016,
|
||||
1.016,
|
||||
1
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"x": [
|
||||
0.417,
|
||||
0.417,
|
||||
0.333
|
||||
],
|
||||
"y": [
|
||||
1.004,
|
||||
1.004,
|
||||
0
|
||||
]
|
||||
},
|
||||
"t": 13,
|
||||
"s": [
|
||||
0,
|
||||
0,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 26,
|
||||
"s": [
|
||||
100,
|
||||
100,
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"d": 1,
|
||||
"ty": "el",
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"nm": "Elliptischer Pfad 1",
|
||||
"mn": "ADBE Vector Shape - Ellipse",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0.858823529412,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "Kontur 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "Transformieren"
|
||||
}
|
||||
],
|
||||
"nm": "Ellipse 1",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "favorite_black_24dp Konturen",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [
|
||||
0.833
|
||||
],
|
||||
"y": [
|
||||
0.833
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"x": [
|
||||
0.167
|
||||
],
|
||||
"y": [
|
||||
0.167
|
||||
]
|
||||
},
|
||||
"t": 9,
|
||||
"s": [
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 25,
|
||||
"s": [
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
28,
|
||||
29,
|
||||
0
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
12,
|
||||
12,
|
||||
0
|
||||
],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [
|
||||
0.295,
|
||||
0.295,
|
||||
0.667
|
||||
],
|
||||
"y": [
|
||||
0.944,
|
||||
0.944,
|
||||
1
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"x": [
|
||||
0.351,
|
||||
0.351,
|
||||
0.167
|
||||
],
|
||||
"y": [
|
||||
2.108,
|
||||
2.108,
|
||||
70.833
|
||||
]
|
||||
},
|
||||
"t": 9,
|
||||
"s": [
|
||||
0,
|
||||
0,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 60,
|
||||
"s": [
|
||||
177.674,
|
||||
177.674,
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
3.78
|
||||
],
|
||||
[
|
||||
-3.08,
|
||||
0
|
||||
],
|
||||
[
|
||||
-1.09,
|
||||
-1.28
|
||||
],
|
||||
[
|
||||
-1.74,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-3.08
|
||||
],
|
||||
[
|
||||
5.15,
|
||||
-4.68
|
||||
]
|
||||
],
|
||||
"o": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-5.15,
|
||||
-4.671
|
||||
],
|
||||
[
|
||||
0,
|
||||
-3.08
|
||||
],
|
||||
[
|
||||
1.74,
|
||||
0
|
||||
],
|
||||
[
|
||||
1.09,
|
||||
-1.28
|
||||
],
|
||||
[
|
||||
3.08,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
3.78
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"v": [
|
||||
[
|
||||
0,
|
||||
9.175
|
||||
],
|
||||
[
|
||||
-1.45,
|
||||
7.856
|
||||
],
|
||||
[
|
||||
-10,
|
||||
-3.675
|
||||
],
|
||||
[
|
||||
-4.5,
|
||||
-9.175
|
||||
],
|
||||
[
|
||||
0,
|
||||
-7.085
|
||||
],
|
||||
[
|
||||
4.5,
|
||||
-9.175
|
||||
],
|
||||
[
|
||||
10,
|
||||
-3.675
|
||||
],
|
||||
[
|
||||
1.45,
|
||||
7.865
|
||||
]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Pfad 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0.858823537827,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"ix": 4
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 5
|
||||
},
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fläche 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
12,
|
||||
12.175
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "Transformieren"
|
||||
}
|
||||
],
|
||||
"nm": "Gruppe 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
@@ -43,7 +43,6 @@
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Zu Favoriten hinzufügen</string>
|
||||
<string name="fav_remove">Aus Favoriten entfernen</string>
|
||||
<string name="distance_format">%.1f km</string>
|
||||
<string name="pref_navigate_use_maps">Navigation sofort starten</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet Navigation direkt</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigationsbutton startet Karten-App mit Position der Ladesäule</string>
|
||||
@@ -79,6 +78,7 @@
|
||||
<string name="map_details">Kartendetails</string>
|
||||
<string name="map_traffic">Verkehr</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Häufig gestellte Fragen</string>
|
||||
<string name="menu_filters_active">Filter aktiv</string>
|
||||
<string name="filters_activated">Filter aktiviert</string>
|
||||
<string name="filters_deactivated">Filter deaktiviert</string>
|
||||
@@ -152,7 +152,9 @@
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe.</string>
|
||||
<string name="welcome_2_title">Auf die Leistung kommt es an</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestatione auf der Karte zeigt dir die maximale Ladeleistung.</string>
|
||||
<string name="welcome_3">EVMap ist kostenlos und Open Source. Du kannst bei GitHub zur Weiterentwicklung beitragen oder die Entwicklung mit Spenden unterstützen. Die entsprechenden Links findest du unter „Über EVMap” im Menü.</string>
|
||||
<string name="welcome_2_detail">Du kannst die Farben im Menü unter “Über EVMap → FAQ” erneut ansehen)</string>
|
||||
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source, ich entwickle es in meiner Freizeit. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Durch die steigende Beliebtheit der App müssen allerdings auch laufende Kosten, z.B. für den Zugriff auf die Datenquellen, gedeckt werden. Daher freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="deleted_filterprofile">„%s” gelöscht</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
<string name="rename">Umbenennen</string>
|
||||
@@ -164,8 +166,6 @@
|
||||
<string name="navigate">Navigieren</string>
|
||||
<string name="verified">Verifiziert</string>
|
||||
<string name="verified_desc">Verifiziert von der %s Community – nicht zwangsläufig auch aktuell verfügbar.</string>
|
||||
<string name="update_060_androidauto_title">Neues Update: Android Auto</string>
|
||||
<string name="update_060_androidauto_text">Mit diesem neuen Update kannst du EVMap nutzen, um Ladestationen in der Nähe auf unterstützen Autos direkt aus Android Auto zu finden. Öffne einfach die EVMap-App aus dem Menü von Android Auto.</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
|
||||
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
|
||||
@@ -187,6 +187,7 @@
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
|
||||
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
|
||||
<string name="chargeprice_duration">(ca. %s)</string>
|
||||
<string name="chargeprice_vehicle">Fahrzeug</string>
|
||||
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
|
||||
<string name="close">schließen</string>
|
||||
@@ -196,6 +197,7 @@
|
||||
<string name="pref_chargeprice_currency">Währung</string>
|
||||
<string name="pref_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
|
||||
<string name="pref_my_tariffs_summary">(werden im Preisvergleich hervorgehoben)</string>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="settings_charger_data">Ladesäulen</string>
|
||||
<string name="pref_data_source">Datenquelle</string>
|
||||
@@ -227,4 +229,12 @@
|
||||
<string name="get_started">Los geht\'s</string>
|
||||
<string name="got_it">Alles klar</string>
|
||||
<string name="lets_go">Und los</string>
|
||||
<string name="crash_report_text">Sorry, anscheinend ist EVMap abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
|
||||
<string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string>
|
||||
<string name="powered_by_mapbox">powered by Mapbox</string>
|
||||
<string name="pref_search_provider">Anbieter für Ortssuche</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Wenn du diese Funktion häufig nutzt, würde ich mich über eine Spende unter \"Über EVMap -> Spenden\" sehr freuen.]]></string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
|
||||
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="ChromeCustomTabsNavigator">
|
||||
<attr name="url" format="reference" />
|
||||
<declare-styleable name="CustomNavigator">
|
||||
<attr name="customDestination" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="MultiSelectDialogPreference">
|
||||
<attr name="showAllButton" format="boolean" />
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
<string name="twitter_handle">\@ev_map</string>
|
||||
<string name="twitter_url">https://twitter.com/ev_map</string>
|
||||
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
|
||||
<string name="report_new_charger_url">https://www.goingelectric.de/stromtankstellen/new/</string>
|
||||
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
|
||||
</resources>
|
||||
@@ -42,7 +42,6 @@
|
||||
<string name="privacy">Privacy Notice</string>
|
||||
<string name="fav_add">Add to favorites</string>
|
||||
<string name="fav_remove">Remove from favorites</string>
|
||||
<string name="distance_format">%.1f km</string>
|
||||
<string name="pref_navigate_use_maps">Start navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigation button starts navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigation button launches maps app with charger location</string>
|
||||
@@ -78,6 +77,7 @@
|
||||
<string name="map_details">Map details</string>
|
||||
<string name="map_traffic">Traffic</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Frequently asked questions</string>
|
||||
<string name="menu_filters_active">Filters active</string>
|
||||
<string name="filters_activated">Filters activated</string>
|
||||
<string name="filters_deactivated">Filters deactivated</string>
|
||||
@@ -151,7 +151,9 @@
|
||||
<string name="welcome_1">Find electric vehicle chargers around you.</string>
|
||||
<string name="welcome_2_title">You\'ve got the power</string>
|
||||
<string name="welcome_2">The color of a charger on the map shows you its maximum charging power.</string>
|
||||
<string name="welcome_3">EVMap is free and Open Source software. You can contribute to the development on GitHub or support me through donations. The corresponding links can be found under “About EVMap” in the menu.</string>
|
||||
<string name="welcome_2_detail">(You can check the colors again under “About EVMap → FAQ” in the menu)</string>
|
||||
<string name="donation_dialog_title">Thank you for using EVMap!</string>
|
||||
<string name="donation_dialog_detail">EVMap is free and Open Source software that I develop in my spare time. Coding contributions on GitHub are very much appreciated. However, due to increasing popularity of the app, I also need to cover some running costs, e.g. for access to the data sources. Therefore, please consider supporting the app through a donation or via GitHub Sponsors.</string>
|
||||
<string name="deleted_filterprofile">Deleted “%s”</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="rename">Rename</string>
|
||||
@@ -163,8 +165,6 @@
|
||||
<string name="navigate">Navigate</string>
|
||||
<string name="verified">verified</string>
|
||||
<string name="verified_desc">Charger verified by a member at the %s community — not necessarily working right now.</string>
|
||||
<string name="update_060_androidauto_title">New update: Android Auto</string>
|
||||
<string name="update_060_androidauto_text">With this new update, you can also use EVMap to find nearby chargers from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
@@ -186,6 +186,7 @@
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
|
||||
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
|
||||
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
|
||||
<string name="chargeprice_duration">(approx. %s)</string>
|
||||
<string name="chargeprice_vehicle">Vehicle</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
|
||||
<string name="close">close</string>
|
||||
@@ -194,6 +195,7 @@
|
||||
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
|
||||
<string name="pref_chargeprice_currency">Currency</string>
|
||||
<string name="pref_my_tariffs">My charging plans</string>
|
||||
<string name="pref_my_tariffs_summary">(will be highlighted in price comparison)</string>
|
||||
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
|
||||
<string name="license">License</string>
|
||||
<string name="settings_charger_data">Charging stations</string>
|
||||
@@ -212,4 +214,12 @@
|
||||
<string name="get_started">Get started</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="lets_go">Let\'s go</string>
|
||||
<string name="crash_report_text">Sorry, it seems that EVMap has crashed. Please send a crash report to the developer.</string>
|
||||
<string name="crash_report_comment_prompt">You can add a comment below:</string>
|
||||
<string name="powered_by_mapbox">powered by Mapbox</string>
|
||||
<string name="pref_search_provider">Place search provider</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Data for place search, especially from Google Maps, is relatively expensive. If you use this feature often, please consider making a donation through \"About EVMap -> Donate\".]]></string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Support EVMap\'s development with a one-time donation</string>
|
||||
<string name="github_sponsors_desc">Support EVMap on GitHub Sponsors</string>
|
||||
</resources>
|
||||
|
||||
@@ -16,11 +16,18 @@
|
||||
|
||||
<Preference
|
||||
android:key="faq"
|
||||
android:title="@string/faq" />
|
||||
android:title="@string/faq"
|
||||
android:summary="@string/faq_desc" />
|
||||
|
||||
<Preference
|
||||
android:key="donate"
|
||||
android:title="@string/donate" />
|
||||
android:title="@string/donate"
|
||||
android:summary="@string/donate_desc" />
|
||||
|
||||
<Preference
|
||||
android:key="github_sponsors"
|
||||
android:title="@string/github_sponsors"
|
||||
android:summary="@string/github_sponsors_desc" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/contact">
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
android:defaultValue="@string/pref_map_provider_default"
|
||||
android:summary="%s" />
|
||||
|
||||
<ListPreference
|
||||
android:key="search_provider"
|
||||
android:title="@string/pref_search_provider"
|
||||
android:entries="@array/pref_search_provider_names"
|
||||
android:entryValues="@array/pref_search_provider_values"
|
||||
android:defaultValue="@string/pref_search_provider_default"
|
||||
android:summary="%s" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="navigate_use_maps"
|
||||
android:title="@string/pref_navigate_use_maps"
|
||||
@@ -55,7 +63,8 @@
|
||||
app:defaultToAll="false" />
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_tariffs"
|
||||
android:title="@string/pref_my_tariffs" />
|
||||
android:title="@string/pref_my_tariffs"
|
||||
android:summary="@string/pref_my_tariffs_summary" />
|
||||
<ListPreference
|
||||
android:key="chargeprice_currency"
|
||||
android:title="@string/pref_chargeprice_currency"
|
||||
|
||||
@@ -10,7 +10,7 @@ buildscript {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user