Compare commits

..

22 Commits
0.7.1 ... 0.7.3

Author SHA1 Message Date
johan12345
498dc63f91 Release 0.7.3 2021-07-03 13:15:15 +02:00
johan12345
c48330dc35 add button to explicitly close the layers menu (#50) 2021-07-03 13:11:00 +02:00
johan12345
ca8abd9b12 fix #92 by using forked version of Mapbox Places Plugin 2021-07-03 12:41:22 +02:00
johan12345
72b2b34af3 disable myTariffsPreference until tariffs are loaded 2021-07-03 11:32:37 +02:00
johan12345
6a7b7a7d39 avoid passing empty string to Android Auto, which caused crash 2021-07-03 11:31:08 +02:00
johan12345
c1af372a06 avoid crash when no browser is installed 2021-07-03 11:23:59 +02:00
johan12345
7946663299 be a bit smarter about bounding boxes for various types of shared locations 2021-06-10 21:05:48 +02:00
johan12345
232aecfe3b add support for another format of geo intent (fixes #90) 2021-06-10 20:41:45 +02:00
johan12345
ac1db7f10d fix lint errors 2021-05-24 10:48:18 +02:00
johan12345
ef99441844 various dependency updates 2021-05-23 23:05:29 +02:00
Johan von Forstner
c4e3534682 Update README.md
fix typo in README
2021-05-23 21:59:38 +02:00
johan12345
d335d7cab0 Release 0.7.2 2021-05-09 15:25:03 +02:00
johan12345
f7c3faa7bd Chargeprice: show my tariffs first in overview 2021-05-09 15:18:35 +02:00
johan12345
1338e2306e Chargeprice: show currently selected currency as summary 2021-05-09 14:45:31 +02:00
johan12345
83a2b42408 Chargeprice: add "my plans" selection preference 2021-05-09 14:44:18 +02:00
johan12345
0ce5938f5b Chargeprice: show provider name only if tariff name doesn't start with it 2021-05-09 14:38:57 +02:00
johan12345
5ab50e04ae README.md: add info about necessary Chargeprice API key 2021-04-28 22:51:05 +02:00
johan12345
ee0fd4e8d8 Chargeprice: store charging range (#86) 2021-04-28 22:47:42 +02:00
johan12345
369b7d9410 Chargeprice: implement currency selection (#86) 2021-04-28 22:41:08 +02:00
johan12345
c9a0b270cd Chargeprice: do not show two error messages if car was not yet selected 2021-04-28 22:38:56 +02:00
johan12345
c8aa64fa7c fix missing German translation 2021-04-24 19:41:39 +02:00
Johan von Forstner
d5b18bd6fb README: Update Features 2021-04-23 08:12:19 +02:00
32 changed files with 495 additions and 96 deletions

View File

@@ -18,6 +18,8 @@ Features
- Realtime availability information (beta)
- Search places
- Favorites list, also with availability information
- Charging price comparison, powered by [Chargeprice.app](https://chargeprice.app)
- Android Auto integration
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
@@ -33,9 +35,10 @@ Development setup
The App is developed using Android Studio.
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs)
as well as for [Google APIs](https://console.developers.google.com/)
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These APIs need to be put into the
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These API keys need to be put into the
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
following content:
@@ -50,5 +53,8 @@ following content:
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
</resources>
```

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 45
versionName "0.7.1"
versionCode 47
versionName "0.7.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -98,16 +98,15 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.5.0-rc01'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation "androidx.activity:activity-ktx:1.2.3"
implementation "androidx.fragment:fragment-ktx:1.3.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation 'androidx.browser:browser:1.3.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
@@ -122,13 +121,13 @@ dependencies {
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
implementation 'com.mapzen.android:lost:3.0.2'
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
// Android Auto
googleImplementation 'androidx.car.app:app:1.0.0-rc01'
googleImplementation 'androidx.car.app:app:1.0.0'
// AnyMaps
def anyMapsVersion = '1f050d860f'
@@ -139,7 +138,7 @@ dependencies {
// Google Maps v3 Beta
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
googleImplementation 'com.android.volley:volley:1.1.1'
googleImplementation 'com.android.volley:volley:1.2.0'
googleImplementation 'com.google.android.gms:play-services-base:17.5.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.5.0'
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
@@ -151,29 +150,29 @@ dependencies {
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Mapbox places (autocomplete)
implementation('com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0') {
// forked this library and included through JitPack to fix https://github.com/mapbox/mapbox-plugins-android/issues/1011
implementation('com.github.johan12345.mapbox-plugins-android:mapbox-android-plugin-places-v9:922bf877f6') {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// navigation library
def nav_version = "2.3.2"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.2.0"
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.2.6"
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "3.0.2"
def billing_version = "4.0.0"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
@@ -189,7 +188,7 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
private static String decode(String s, String key) {

View File

@@ -546,6 +546,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
if (isNotEmpty()) append(" · ")
append(it)
}
}.ifEmpty {
carContext.getString(R.string.unknown_operator)
}
setTitle(operatorText)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -22,6 +23,7 @@ import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.fragment.MapFragment
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
import net.vonforst.evmap.utils.getLocationFromIntent
const val REQUEST_LOCATION_PERMISSION = 1
@@ -79,28 +81,25 @@ class MapsActivity : AppCompatActivity() {
checkPlayServices(this)
if (intent?.scheme == "geo") {
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
val query = intent.data?.query?.split("=")?.get(1)
val coords = pos?.split(",")?.map { it.toDoubleOrNull() }
val coords = getLocationFromIntent(intent)
if (coords != null && coords.size == 2) {
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
if (lat != null && lon != null && lat != 0.0 && lon != 0.0) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocation(lat, lon))
.createPendingIntent()
deepLink.send()
} else if (query != null && query.isNotEmpty()) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocationByName(query))
.createPendingIntent()
deepLink.send()
}
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocation(lat, lon))
.createPendingIntent()
deepLink.send()
} else if (query != null && query.isNotEmpty()) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocationByName(query))
.createPendingIntent()
deepLink.send()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
@@ -164,7 +163,16 @@ class MapsActivity : AppCompatActivity() {
.build()
)
.build()
intent.launchUrl(this, Uri.parse(url))
try {
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make(
cb.getRootView(),
R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
fun shareUrl(url: String) {

View File

@@ -107,6 +107,16 @@ class ChargepriceAdapter() :
field = value
notifyDataSetChanged()
}
var myTariffs: Set<String>? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffsAll: Boolean? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
@@ -127,7 +137,11 @@ class ChargepriceAdapter() :
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
super.bind(holder, item)
(holder.binding as ItemChargepriceBinding).meta = meta
(holder.binding as ItemChargepriceBinding).apply {
this.meta = this@ChargepriceAdapter.meta
this.myTariffs = this@ChargepriceAdapter.myTariffs
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
}
}
}

View File

@@ -26,6 +26,9 @@ interface ChargepriceApi {
@GET("vehicles")
suspend fun getVehicles(): ArrayDocument<ChargepriceCar>
@GET("tariffs")
suspend fun getTariffs(): ArrayDocument<ChargepriceTariff>
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")

View File

@@ -70,13 +70,46 @@ data class ChargepriceOptions(
)
@JsonApi(type = "tariff")
data class ChargepriceTariff(
val provider: String,
val name: String,
@field:Json(name = "direct_payment") val directPayment: Boolean,
@field:Json(name = "provider_customer_tariff") val providerCustomerTariff: Boolean,
@field:Json(name = "charge_card_id") val chargeCardId: String // GE charge card ID
) : Resource()
class ChargepriceTariff() : Resource() {
lateinit var provider: String
lateinit var name: String
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
@field:Json(name = "supported_cuntries")
lateinit var supportedCountries: Set<String>
@field:Json(name = "charge_card_id")
lateinit var chargeCardId: String // GE charge card ID
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceTariff
if (provider != other.provider) return false
if (name != other.name) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (supportedCountries != other.supportedCountries) return false
if (chargeCardId != other.chargeCardId) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + supportedCountries.hashCode()
result = 31 * result + chargeCardId.hashCode()
return result
}
}
@JsonApi(type = "car")
class ChargepriceCar : Resource() {
@@ -146,6 +179,8 @@ class ChargePrice : Resource(), Equatable, Cloneable {
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
var tariff: HasOne<ChargepriceTariff>? = null
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
@@ -212,6 +247,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
tariffName = this@ChargePrice.tariffName
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
}
}
}

View File

@@ -112,6 +112,12 @@ class ChargepriceFragment : DialogFragment() {
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.meta = it?.data
}
vm.myTariffs.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffs = it
}
vm.myTariffsAll.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffsAll = it
}
val connectorsAdapter = CheckableConnectorAdapter()
@@ -168,6 +174,7 @@ class ChargepriceFragment : DialogFragment() {
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
when (res?.status) {
Status.ERROR -> {
if (vm.vehicle.value == null) return@observe
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(

View File

@@ -77,6 +77,7 @@ import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.ClusterIconGenerator
import net.vonforst.evmap.ui.MarkerAnimator
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.boundingBox
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.*
@@ -224,15 +225,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
setupObservers()
setupClickListeners()
setupAdapters()
val navController = findNavController()
(activity as? MapsActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
val prefs = PreferenceDataSource(requireContext())
val navController = findNavController()
if (!prefs.welcomeDialogShown) {
try {
navController.navigate(R.id.action_map_to_welcome)
@@ -252,6 +248,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
super.onResume()
val hostActivity = activity as? MapsActivity ?: return
hostActivity.fragmentCallback = this
val navController = findNavController()
binding.toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
vm.reloadPrefs()
if (requestingLocationUpdates && ContextCompat.checkSelfPermission(
requireContext(),
@@ -288,6 +291,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.fabLayers.setOnClickListener {
openLayersMenu()
}
binding.layers.btnClose.setOnClickListener {
closeLayersMenu()
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value?.data
if (charger != null) {
@@ -782,7 +788,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
positionSet = true
} else if (lat != null && lon != null) {
// show given position
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
val latLng = LatLng(lat, lon)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
if (chargerId != null) {
@@ -802,7 +809,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
} else {
// mark location as search result
vm.searchResult.value = PlaceWithBounds(LatLng(lat, lon), null)
vm.searchResult.value = PlaceWithBounds(latLng, boundingBox(latLng, 750.0))
}
positionSet = true
@@ -815,7 +822,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val latLng = LatLng(it.latitude, it.longitude)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
vm.searchResult.value = PlaceWithBounds(latLng, null)
val bboxSize = if (it.subAdminArea != null) {
750.0 // this is a place within a city
} else if (it.adminArea != null && it.adminArea != it.featureName) {
4000.0 // this is a city
} else if (it.adminArea != null) {
100000.0 // this is a top-level administrative area (i.e. state)
} else {
500000.0 // this is a country
}
vm.searchResult.value = PlaceWithBounds(latLng, boundingBox(latLng, bboxSize))
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
@@ -32,6 +33,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
})
private lateinit var myVehiclePreference: ListPreference
private lateinit var myTariffsPreference: MultiSelectListPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -44,7 +46,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
(requireActivity() as MapsActivity).appBarConfiguration
)
myVehiclePreference = findPreference<ListPreference>("chargeprice_my_vehicle")!!
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
vm.vehicles.observe(viewLifecycleOwner) { res ->
res.data?.let { cars ->
@@ -57,6 +59,33 @@ class SettingsFragment : PreferenceFragmentCompat(),
?.let { "${it.brand} ${it.name}" }
}
}
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
myTariffsPreference.isEnabled = false
vm.tariffs.observe(viewLifecycleOwner) { res ->
res.data?.let { tariffs ->
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
myTariffsPreference.entries = tariffs.map {
if (!it.name.startsWith(it.provider)) {
"${it.provider} ${it.name}"
} else {
it.name
}
}.toTypedArray()
myTariffsPreference.isEnabled = true
updateMyTariffsSummary()
}
}
}
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)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -89,6 +118,9 @@ class SettingsFragment : PreferenceFragmentCompat(),
}
}
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
}
}

View File

@@ -111,6 +111,18 @@ class PreferenceDataSource(val context: Context) {
.apply()
}
var chargepriceMyTariffs: Set<String>?
get() = sp.getStringSet("chargeprice_my_tariffs", null)
set(value) {
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
}
var chargepriceMyTariffsAll: Boolean
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
set(value) {
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
}
var chargepriceNoBaseFee: Boolean
get() = sp.getBoolean("chargeprice_no_base_fee", false)
set(value) {
@@ -122,4 +134,21 @@ class PreferenceDataSource(val context: Context) {
set(value) {
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
}
var chargepriceCurrency: String
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
set(value) {
sp.edit().putString("chargeprice_currency", value).apply()
}
var chargepriceBatteryRange: List<Float>
get() = listOf(
sp.getFloat("chargeprice_battery_range_min", 20f),
sp.getFloat("chargeprice_battery_range_max", 80f),
)
set(value) {
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
.putFloat("chargeprice_battery_range_max", value[1])
.apply()
}
}

View File

@@ -255,7 +255,7 @@ fun currency(currency: String): String {
"USD" -> "$"
"DKK", "SEK", "NOK" -> "kr."
"PLN" -> ""
"CHF" -> "Fr."
"CHF" -> "Fr. "
"CZK" -> ""
"GBP" -> "£"
"HRK" -> "kn"
@@ -291,4 +291,15 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
@BindingAdapter("app:tint")
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}
@BindingAdapter("myTariffsBackground")
fun myTariffsBackground(view: View, myTariff: Boolean) {
if (myTariff) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
} else {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
}
}
}

View File

@@ -0,0 +1,31 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.MultiSelectListPreference
import net.vonforst.evmap.fragment.MultiSelectDialog
class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
MultiSelectListPreference(ctx, attrs) {
override fun onClick() {
val dialog =
MultiSelectDialog.getInstance(
title.toString(),
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
if (all) entryValues.map { it.toString() }.toSet() else values,
emptySet()
)
dialog.okListener = { selected ->
all = selected == entryValues.toSet()
values = selected
}
dialog.show((context as AppCompatActivity).supportFragmentManager, null)
}
var all: Boolean
get() = sharedPreferences.getBoolean(key + "_all", true)
set(value) {
sharedPreferences.edit().putBoolean(key + "_all", value).apply()
}
}

View File

@@ -1,6 +1,9 @@
package net.vonforst.evmap.utils
import android.content.Intent
import android.location.Location
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlin.math.*
/**
@@ -12,6 +15,12 @@ fun Location.plusMeters(dx: Double, dy: Double): Pair<Double, Double> {
return Pair(lat, lon)
}
fun LatLng.plusMeters(dx: Double, dy: Double): LatLng {
val lat = this.latitude + (180 / Math.PI) * (dx / 6378137.0)
val lon = this.longitude + (180 / Math.PI) * (dy / 6378137.0) / cos(Math.toRadians(lat))
return LatLng(lat, lon)
}
const val earthRadiusM = 6378137.0
/**
@@ -31,4 +40,38 @@ fun distanceBetween(
val a = sin(dLat / 2).pow(2.0) + sin(dLon / 2).pow(2.0) * cos(originLat) * cos(destinationLat)
val c = 2 * asin(sqrt(a))
return earthRadiusM * c
}
}
fun getLocationFromIntent(intent: Intent): List<Double>? {
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
var coords = stringToCoords(pos)
if (coords != null) {
return coords
}
val query = intent.data?.query?.split("=")?.get(1)
coords = stringToCoords(query)
if (coords != null) {
return coords
} else {
return null
}
}
internal fun stringToCoords(s: String?): List<Double>? {
if (s == null) return null
val coords = s.split(",").mapNotNull { it.toDoubleOrNull() }
return if (coords.size == 2 && !(coords[0] == 0.0 && coords[1] == 0.0)) {
coords
} else {
null
}
}
fun boundingBox(pos: LatLng, sizeMeters: Double): LatLngBounds {
return LatLngBounds(
pos.plusMeters(-sizeMeters, -sizeMeters),
pos.plusMeters(sizeMeters, sizeMeters)
)
}

View File

@@ -67,7 +67,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val batteryRange: MutableLiveData<List<Float>> by lazy {
MutableLiveData<List<Float>>().apply {
value = listOf(20f, 80f)
value = prefs.chargepriceBatteryRange
observeForever {
prefs.chargepriceBatteryRange = it
}
}
}
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
@@ -112,6 +115,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
} else if (cps.status == Status.LOADING) {
value = Resource.loading(null)
} else {
val myTariffs = prefs.chargepriceMyTariffs
value = Resource.success(cps.data!!.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power }
@@ -122,13 +126,30 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
chargepointPrices = filteredPrices
}
}
}.filterNotNull().sortedBy { it.chargepointPrices.first().price })
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
)
}
}
}
}
}
val myTariffs: LiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyTariffs
}
}
val myTariffsAll: LiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = prefs.chargepriceMyTariffsAll
}
}
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
listOf(chargePriceMeta, chargepoint).forEach {
@@ -173,7 +194,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, getChargepriceLanguage())
val meta =

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import java.io.IOException
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
@@ -20,6 +21,13 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
}
}
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
value = Resource.loading(null)
loadTariffs()
}
}
private fun loadVehicles() {
viewModelScope.launch {
try {
@@ -30,4 +38,15 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
}
}
}
private fun loadTariffs() {
viewModelScope.launch {
try {
val result = api.getTariffs()
tariffs.value = Resource.success(result)
} catch (e: IOException) {
tariffs.value = Resource.error(e.message, null)
}
}
}
}

View File

@@ -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="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -11,6 +11,8 @@
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="java.util.Set" />
<variable
name="item"
type="ChargePrice" />
@@ -18,6 +20,14 @@
<variable
name="meta"
type="ChargepriceChargepointMeta" />
<variable
name="myTariffs"
type="Set&lt;String>" />
<variable
name="myTariffsAll"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -27,7 +37,7 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:background="?selectableItemBackground">
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
<TextView
android:id="@+id/txtTariff"
@@ -50,7 +60,7 @@
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.provider.equals(item.tariffName)}"
app:goneUnless="@{!item.tariffName.startsWith(item.provider)}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"

View File

@@ -60,7 +60,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?colorControlNormal"
app:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/handle"
app:layout_constraintTop_toTopOf="parent"
@@ -73,7 +73,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?colorControlNormal"
app:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnDelete"
app:layout_constraintTop_toTopOf="parent"

View File

@@ -31,7 +31,7 @@
android:layout_marginEnd="16dp"
android:text="@string/map_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnClose"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -98,5 +98,18 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView23" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close"
app:tint="?colorControlNormal" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -13,31 +13,31 @@
<action
android:id="@+id/action_map_to_galleryFragment"
app:destination="@id/gallery"
app:enterAnim="@anim/fragment_fade_enter"
app:exitAnim="@anim/fragment_fade_exit"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
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"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_filterProfilesFragment"
app:destination="@id/filter_profiles"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_chargepriceFragment"
app:destination="@id/chargeprice"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_welcome"
app:destination="@id/welcome" />
@@ -91,10 +91,10 @@
<action
android:id="@+id/action_chargeprice_to_settingsFragment"
app:destination="@id/settings"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
</dialog>
<fragment
android:id="@+id/donate"

View File

@@ -4,6 +4,7 @@
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Anschlüsse</string>
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
<string name="no_browser_app_found">Kein Webbrowser gefunden</string>
<string name="address">Adresse</string>
<string name="operator">Betreiber</string>
<string name="network">Verbund</string>
@@ -186,6 +187,28 @@
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_title">Preisvergleich</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="pref_chargeprice_currency">Währung</string>
<string name="pref_my_tariffs">Meine Tarife</string>
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
<string-array name="pref_chargeprice_currency_names">
<item>Schweizer Franken (CHF)</item>
<item>Tschechische Krone (CZK)</item>
<item>Dänische Krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Britisches Pfund (GBP)</item>
<item>Kroatische Kuna (HRK)</item>
<item>Ungarischer Forint (HUF)</item>
<item>Isländische Krone (ISK)</item>
<item>Norwegische Krone (NOK)</item>
<item>Polnischer Złoty (PLN)</item>
<item>Schwedische Krone (SEK)</item>
<item>US-Dollar (USD)</item>
</string-array>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d Tarif ausgewählt</item>
<item quantity="other">%d Tarife ausgewählt</item>
</plurals>
<string name="unknown_operator">Unbekannter Betreiber</string>
</resources>

View File

@@ -20,4 +20,32 @@
<item>on</item>
<item>off</item>
</string-array>
<string-array name="pref_chargeprice_currency_names">
<item>Swiss franc (CHF)</item>
<item>Czech koruna (CZK)</item>
<item>Danish krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Pound sterling (GBP)</item>
<item>Croatian kuna (HRK)</item>
<item>Hungarian forint (HUF)</item>
<item>Icelandic króna (ISK)</item>
<item>Norwegian krone (NOK)</item>
<item>Polish złoty (PLN)</item>
<item>Swedish krona (SEK)</item>
<item>US dollar (USD)</item>
</string-array>
<string-array name="pref_chargeprice_currency_values" donottranslate="true">
<item>CHF</item>
<item>CZK</item>
<item>DKK</item>
<item>EUR</item>
<item>GBP</item>
<item>HRK</item>
<item>HUF</item>
<item>ISK</item>
<item>NOK</item>
<item>PLN</item>
<item>SEK</item>
<item>USD</item>
</string-array>
</resources>

View File

@@ -3,6 +3,7 @@
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectors</string>
<string name="no_maps_app_found">No navigation app found</string>
<string name="no_browser_app_found">No web browser found</string>
<string name="address">Address</string>
<string name="operator">Operator</string>
<string name="network">Network</string>
@@ -187,4 +188,12 @@
<string name="chargeprice_title">Prices</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<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="chargeprice_all_tariffs_selected">all plans selected</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan selected</item>
<item quantity="other">%d plans selected</item>
</plurals>
<string name="unknown_operator">Unknown operator</string>
</resources>

View File

@@ -42,6 +42,16 @@
<ListPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"

View File

@@ -9,4 +9,11 @@ class LocationUtilsTest {
fun testDistanceBetween() {
assertEquals(129521.08, distanceBetween(54.0, 9.0, 53.0, 8.0), 0.01)
}
@Test
fun testStringToCoords() {
assertEquals(listOf(52.515577, 13.379907), stringToCoords("52.515577,13.379907"))
assertEquals(null, stringToCoords("52.515577,13.379907,57.123456"))
assertEquals(null, stringToCoords("Hello, world."))
}
}

View File

@@ -1,18 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.21'
ext.about_libs_version = '8.1.1'
ext.kotlin_version = '1.4.32'
ext.about_libs_version = '8.8.5'
ext.nav_version = '2.3.5'
repositories {
google()
jcenter()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
def nav_version = "2.3.0-alpha04"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong
@@ -23,7 +23,9 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
//noinspection JcenterRepositoryObsolete
jcenter() // still required for https://github.com/kamikat/moshi-jsonapi
maven { url 'https://jitpack.io' }
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
flatDir {

View File

@@ -0,0 +1,4 @@
Verbesserungen für Chargeprice.app-Integration:
- Währung kann in den Einstellungen gewählt werden
- Ausgewählter Ladebereich wird gespeichert
- Eigene Tarife können in den Einstellungen ausgewählt werden

View File

@@ -0,0 +1,3 @@
Verschiedene Abstürze behoben
Bessere Unterstützung für geteilte Standorte aus anderen Apps
F-Droid-Version: Suchfeld wird beim Öffnen automatisch aktiviert

View File

@@ -0,0 +1,4 @@
Improvements for Chargeprice.app integration:
- Currency selection in settings
- Save selected charging range
- Own charging plans can be selected in settings

View File

@@ -0,0 +1,3 @@
Fixed various crashes
Improved support for shared locations from other apps
F-Droid version: Search field will be focused automatically when opened

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip