Compare commits

..

13 Commits
1.4.4 ... 1.4.6

Author SHA1 Message Date
johan12345
2aa1fcf5bd Release 1.4.6 2023-02-01 18:56:15 +01:00
johan12345
221e5f49bc catch JsonDataExceptions from fronyx API 2023-02-01 18:54:35 +01:00
johan12345
df6f26ad56 fix import 2023-01-29 19:38:08 +01:00
johan12345
1210efd3b9 MapFragment: update map bottom padding when bottom sheet comes up 2023-01-29 18:54:02 +01:00
johan12345
097be8c92b get rid of some warnings 2023-01-28 22:33:49 +01:00
johan12345
16031884ac upgrade dependencies
Android Studio 2022.1.1
resourcesPlaceholders plugin broke - removed it for now
2023-01-28 22:00:52 +01:00
johan12345
c0b4c56eda Release 1.4.5 2023-01-20 20:10:57 +01:00
Hosted Weblate
9587ee948d Update translation files
Updated by "Squash Git commits" hook in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 83.3% (30 of 36 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nb_NO/
Translation: EVMap/Android (strings specific to Google Play variant)
2023-01-20 19:13:16 +01:00
johan12345
890eec4419 FilterFragment: add button to reset current settings 2023-01-20 19:12:52 +01:00
johan12345
c972c871d4 add icons to filter popup menu 2023-01-20 18:38:06 +01:00
johan12345
e4da902430 GooglePlacesAutocompleteProvider: fix crash on network error 2023-01-19 22:28:13 +01:00
johan12345
7a5d4b4107 fix NPE 2023-01-08 12:48:23 +01:00
johan12345
80642b1731 fix infinite recursion in Utils.max 2023-01-08 12:45:31 +01:00
47 changed files with 248 additions and 107 deletions

View File

@@ -8,7 +8,6 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'de.timfreiheit.resourceplaceholders'
def supportedLocales = "en,de,fr,nb-rNO"
@@ -21,11 +20,11 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 156
versionName "1.4.4"
versionCode 160
versionName "1.4.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
resConfigs supportedLocales.split(',')
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
}
@@ -104,9 +103,6 @@ android {
unitTests.includeAndroidResources true
}
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
@@ -159,14 +155,14 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
@@ -196,7 +192,7 @@ dependencies {
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
// AnyMaps
def anyMapsVersion = 'a9b3dd7d99'
def anyMapsVersion = '7fdcf50fc4'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
@@ -210,8 +206,8 @@ dependencies {
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
// Google Places
googleImplementation 'com.google.android.libraries.places:places:2.7.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
@@ -226,7 +222,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.4.3"
def room_version = "2.5.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
@@ -257,12 +253,12 @@ dependencies {
testGoogleImplementation 'org.robolectric:robolectric:4.9'
testGoogleImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.0'
}
private static String decode(String s, String key) {

View File

@@ -206,7 +206,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private fun loadPrices(model: Model?) {
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
val manufacturer = model?.manufacturer?.value
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
lifecycleScope.launch {

View File

@@ -335,7 +335,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
availabilities[charger.id]?.second?.let { av ->
val status = av.status.values.flatten()
val available = availabilityText(status)
val total = charger.chargepoints.sumBy { it.count }
val total = charger.chargepoints.sumOf { it.count }
if (text.isNotEmpty()) text.append(" · ")
text.append(
@@ -476,7 +476,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
val location = location
val heading = heading?.orientations?.value?.get(0)
?: if (location?.hasBearing() == true) location.bearing else null
return heading?.let { heading ->
return heading?.let {
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
chargers?.filter {

View File

@@ -46,7 +46,7 @@ class PermissionScreen(
}
private fun requestPermissions() {
carContext.requestPermissions(permissions) { granted, rejected ->
carContext.requestPermissions(permissions) { granted, _ ->
if (granted.containsAll(permissions)) {
screenManager.pop()
} else {

View File

@@ -148,6 +148,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
}
private suspend fun loadNewList(query: String) {
val location = location?.let { LatLng.fromLocation(it) }
for (provider in providers) {
try {
recentResults.clear()
@@ -161,7 +162,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
}
recentResults.addAll(recentPlaces)
resultList =
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
recentPlaces.map { it.asAutocompletePlace(location) }
invalidate()
// if we already have enough results or the query is short, stop here
@@ -170,7 +171,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
// then search online
val recentIds = recentPlaces.map { it.id }
resultList = withContext(Dispatchers.IO) {
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
(resultList!! + provider.autocomplete(query, location)
.filter { !recentIds.contains(it.id) }).take(maxItems)
}
invalidate()

View File

@@ -15,10 +15,7 @@ import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.EXTRA_DONATE
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.*
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
@@ -718,7 +715,7 @@ class DeveloperOptionsScreen(ctx: CarContext) : Screen(ctx) {
val hostPackage = carContext.hostInfo?.packageName
val hostVersion = hostPackage?.let {
try {
carContext.packageManager.getPackageInfo(it, 0).versionName
carContext.packageManager.getPackageInfoCompat(it).versionName
} catch (e: NameNotFoundException) {
null
}

View File

@@ -19,6 +19,7 @@ import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.getPackageInfoCompat
import java.util.*
import kotlin.math.roundToInt
@@ -196,7 +197,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
}
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")
}

View File

@@ -7,6 +7,7 @@ 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.common.api.CommonStatusCodes
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.tasks.Tasks.await
@@ -19,6 +20,7 @@ import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRe
import com.google.android.libraries.places.api.net.PlacesStatusCodes
import kotlinx.coroutines.tasks.await
import net.vonforst.evmap.R
import java.io.IOException
import java.util.concurrent.ExecutionException
import kotlin.math.sqrt
@@ -58,6 +60,13 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
if (cause is ApiException) {
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
throw ApiUnavailableException()
} else if (cause.statusCode in listOf(
CommonStatusCodes.NETWORK_ERROR,
CommonStatusCodes.TIMEOUT, CommonStatusCodes.RECONNECTION_TIMED_OUT,
CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE
)
) {
throw IOException(cause)
}
}
throw e

View File

@@ -36,4 +36,6 @@
<string name="sounds_cool">den er grei</string>
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
<string name="loading">Laster inn …</string>
<string name="auto_multipage_goto">Side %d</string>
<string name="auto_multipage">(%d/%d)</string>
</resources>

View File

@@ -76,7 +76,7 @@ class MapsActivity : AppCompatActivity(),
val navView = findViewById<NavigationView>(R.id.nav_view)
navView.setupWithNavController(navController)
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
ViewCompat.setOnApplyWindowInsetsListener(navView) { _, insets ->
val header = navView.getHeaderView(0)
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
insets

View File

@@ -1,8 +1,11 @@
package net.vonforst.evmap
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.text.*
import android.text.style.StyleSpan
@@ -72,7 +75,7 @@ fun max(a: Int?, b: Int?): Int? {
* otherwise the non-null value or null
*/
return if (a != null && b != null) {
max(a, b)
kotlin.math.max(a, b)
} else {
a ?: b
}
@@ -88,4 +91,11 @@ const val meterPerFt = 0.3048
fun shouldUseImperialUnits(): Boolean {
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
}
}
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
} else {
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
}

View File

@@ -1,7 +1,6 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
@@ -166,7 +165,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked) {
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
root.post {
@@ -205,7 +204,7 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
root.setOnCheckedChangeListener { _, checked: Boolean ->
if (checked && item != checkedItem) {
checkedItem = item
root.post {

View File

@@ -25,7 +25,7 @@ class FilterProfilesAdapter(
super.bind(holder, item)
val binding = holder.binding as ItemFilterProfileBinding
binding.handle.setOnTouchListener { v, event ->
binding.handle.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_DOWN) {
dragHelper.startDrag(holder)
}

View File

@@ -71,19 +71,19 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
var chargepoints = chargepoints
var cpts = chargepoints
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
var geTypes = chargepoints.map { it.type }.distinct().toSet()
var geTypes = cpts.map { it.type }.distinct().toSet()
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
Chargepoint.SCHUKO
)) {
// If charger has household plugs and other plugs, try removing the household plugs
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
cpts = cpts.filter { it.type != Chargepoint.SCHUKO }
}
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
@@ -93,14 +93,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
val powers = connsOfType.map { it.value.first }.distinct().sorted()
// find corresponding powers in GE data
val gePowers =
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
cpts.filter { equivalentPlugTypes(it.type).any { it == type } }
.mapNotNull { it.power }.distinct().sorted()
// if the distinct number of powers is the same, try to match.
if (powers.size == gePowers.size) {
gePowers.zip(powers).map { (gePower, power) ->
val chargepoint =
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
cpts.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
val ids = connsOfType.filter { it.value.first == power }.keys
if (chargepoint.count != ids.size) {
throw AvailabilityDetectorException("chargepoints do not match")
@@ -108,7 +108,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
chargepoint to ids
}
} else if (powers.size == 1 && gePowers.size == 2
&& chargepoints.sumOf { it.count } == connsOfType.size
&& cpts.sumOf { it.count } == connsOfType.size
) {
// special case: dual charger(s) with load balancing
// GoingElectric shows 2 different powers, NewMotion just one
@@ -116,7 +116,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
var i = 0
gePowers.map { gePower ->
val chargepoint =
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
cpts.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
val ids = allIds.subList(i, i + chargepoint.count).toSet()
i += chargepoint.count
chargepoint to ids

View File

@@ -51,7 +51,7 @@ interface ChargecloudApi {
)
companion object {
fun create(client: OkHttpClient, baseUrl: String? = null): ChargecloudApi {
fun create(client: OkHttpClient, baseUrl: String): ChargecloudApi {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())

View File

@@ -140,7 +140,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
connectorStatus.forEach { (connector, statusStr, evseId) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2_UNKNOWN
"type1" -> Chargepoint.TYPE_1

View File

@@ -71,10 +71,10 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
private val clazz: Class<*>
) : JsonAdapter<T>() {
class Factory() : JsonAdapter.Factory {
class Factory : JsonAdapter.Factory {
override fun create(
type: Type,
annotations: Set<Annotation>?,
annotations: Set<Annotation>,
moshi: Moshi
): JsonAdapter<Any>? {
val clazz = Types.getRawType(type)

View File

@@ -399,10 +399,10 @@ class GoingElectricApiWrapper(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as GEReferenceData
val plugs = referenceData.plugs
val networks = referenceData.networks
val chargeCards = referenceData.chargecards
val refData = referenceData as GEReferenceData
val plugs = refData.plugs
val networks = refData.networks
val chargeCards = refData.chargecards
val plugMap = plugs.map { plug ->
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))

View File

@@ -120,7 +120,7 @@ class OpenChargeMapApiWrapper(
zoom: Float,
filters: FilterValues?,
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val minPower = filters?.getSliderValue("min_power")?.toDouble()
val minConnectors = filters?.getSliderValue("min_connectors")
@@ -160,7 +160,7 @@ class OpenChargeMapApiWrapper(
minPower,
connectorsVal,
minConnectors,
referenceData,
refData,
zoom
)
return Resource.success(result)
@@ -176,7 +176,7 @@ class OpenChargeMapApiWrapper(
zoom: Float,
filters: FilterValues?
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val minPower = filters?.getSliderValue("min_power")?.toDouble()
val minConnectors = filters?.getSliderValue("min_connectors")
@@ -214,7 +214,7 @@ class OpenChargeMapApiWrapper(
minPower,
connectorsVal,
minConnectors,
referenceData,
refData,
zoom
)
return Resource.success(result)
@@ -254,11 +254,11 @@ class OpenChargeMapApiWrapper(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
try {
val response = api.getChargepointDetail(id)
if (response.isSuccessful && response.body()?.size == 1) {
return Resource.success(response.body()!![0].convert(referenceData, true))
return Resource.success(response.body()!![0].convert(refData, true))
} else {
return Resource.error(response.message(), null)
}
@@ -284,10 +284,10 @@ class OpenChargeMapApiWrapper(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as OCMReferenceData
val refData = referenceData as OCMReferenceData
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
return listOf(
// supported by OCM API

View File

@@ -44,8 +44,7 @@ class FavoritesFragment : Fragment() {
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
viewModelFactory {
FavoritesViewModel(
requireActivity().application,
getString(R.string.goingelectric_key)
requireActivity().application
)
}
})

View File

@@ -98,6 +98,12 @@ class FilterFragment : Fragment(), MenuProvider {
saveProfile()
true
}
R.id.menu_reset -> {
lifecycleScope.launch {
vm.resetValues()
}
true
}
else -> false
}
}
@@ -114,7 +120,7 @@ class FilterFragment : Fragment(), MenuProvider {
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
.setPositiveButton(R.string.ok) { _, _ ->
if (input.text.isBlank()) {
saveProfile(true)
} else {
@@ -124,7 +130,7 @@ class FilterFragment : Fragment(), MenuProvider {
}
}
}
.setNegativeButton(R.string.cancel) { di, button ->
.setNegativeButton(R.string.cancel) { _, _ ->
}
}

View File

@@ -188,12 +188,12 @@ class FilterProfilesFragment : Fragment() {
dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
vm.update(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { di, button ->
.setNegativeButton(R.string.cancel) { _, _ ->
}
}

View File

@@ -91,6 +91,7 @@ import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.contains
import kotlin.collections.set
import kotlin.math.min
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
@@ -197,7 +198,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { v, insets ->
) { _, insets ->
ViewCompat.onApplyWindowInsets(binding.root, insets)
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
@@ -465,7 +466,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
)
}
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
binding.search.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
binding.search.keyListener = searchKeyListener
binding.search.text = binding.search.text // workaround to fix copy/paste
@@ -553,7 +554,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (bottomSheetBehavior.state == STATE_HIDDEN) {
map?.setPadding(0, mapTopPadding, 0, 0)
} else {
val height = binding.root.height - bottomSheet.top
map?.setPadding(
0,
mapTopPadding,
0,
min(bottomSheetBehavior.peekHeight, height)
)
}
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
@@ -561,9 +572,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateBackPressedCallback()
if (vm.layersMenuOpen.value!! && newState !in listOf(
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
STATE_SETTLING,
STATE_HIDDEN,
STATE_COLLAPSED
)
) {
closeLayersMenu()
@@ -1189,6 +1200,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
)
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
MenuCompat.setGroupDividerEnabled(popup.menu, true)
popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_edit_filters -> {

View File

@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
items = data.entries.toList()
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
.sortedBy { it.value.lowercase(Locale.getDefault()) }
.sortedBy {
when {
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
@@ -117,7 +117,7 @@ private fun search(
): List<MultiSelectItem> {
return items.filter { item ->
// search for string within name
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
}
}

View File

@@ -111,7 +111,7 @@ data class ChargeLocation(
// check if there is more than one plug for any connector type
val chargepointsPerConnector =
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumOf { it.count } }
return chargepointsPerConnector.any { it > 1 }
}
@@ -127,13 +127,13 @@ data class ChargeLocation(
return variants.map { variant ->
val count = chargepoints
.filter { it.type == variant.type && it.power == variant.power }
.sumBy { it.count }
.sumOf { it.count }
Chargepoint(variant.type, variant.power, count)
}
}
val totalChargepoints: Int
get() = chargepoints.sumBy { it.count }
get() = chargepoints.sumOf { it.count }
fun formatChargepoints(sp: StringProvider): String {
return chargepointsMerged.map {

View File

@@ -1,29 +1,49 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.liveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.*
import net.vonforst.evmap.model.*
@Dao
abstract class FilterValueDao {
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getBooleanFilterValues(
protected abstract suspend fun getBooleanFilterValuesAsync(
profile: Long,
dataSource: String
): List<BooleanFilterValue>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getMultipleChoiceFilterValues(
protected abstract suspend fun getMultipleChoiceFilterValuesAsync(
profile: Long,
dataSource: String
): List<MultipleChoiceFilterValue>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract suspend fun getSliderFilterValues(
protected abstract suspend fun getSliderFilterValuesAsync(
profile: Long,
dataSource: String
): List<SliderFilterValue>
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getBooleanFilterValues(
profile: Long,
dataSource: String
): LiveData<List<BooleanFilterValue>>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getMultipleChoiceFilterValues(
profile: Long,
dataSource: String
): LiveData<List<MultipleChoiceFilterValue>>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getSliderFilterValues(
profile: Long,
dataSource: String
): LiveData<List<SliderFilterValue>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
@@ -58,15 +78,32 @@ abstract class FilterValueDao {
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
emptyList()
} else {
getBooleanFilterValues(filterStatus, dataSource) +
getMultipleChoiceFilterValues(filterStatus, dataSource) +
getSliderFilterValues(filterStatus, dataSource)
getBooleanFilterValuesAsync(filterStatus, dataSource) +
getMultipleChoiceFilterValuesAsync(filterStatus, dataSource) +
getSliderFilterValuesAsync(filterStatus, dataSource)
}
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
emit(null)
emit(getFilterValuesAsync(filterStatus, dataSource))
}
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>?> =
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
MutableLiveData(emptyList())
} else {
MediatorLiveData<List<FilterValue>?>().apply {
value = null
val sources = listOf(
getBooleanFilterValues(filterStatus, dataSource),
getMultipleChoiceFilterValues(filterStatus, dataSource),
getSliderFilterValues(filterStatus, dataSource)
)
for (source in sources) {
addSource(source) {
val values = sources.map { it.value }
if (values.all { it != null }) {
value = values.filterNotNull().flatten()
}
}
}
}
}
@Transaction
open suspend fun insert(vararg values: FilterValue) {

View File

@@ -211,12 +211,12 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
val bubbleBounds = bubbleBounds ?: return
val graphBounds = graphBounds ?: return
val data = data.toList()
val d = data.toList()
if (data.size <= selectedBar) return
if (d.size <= selectedBar) return
canvas.apply {
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
val (t, v) = data[selectedBar]
val (t, v) = d[selectedBar]
val tformat = context.getString(
R.string.prediction_time_colon,
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)

View File

@@ -121,6 +121,7 @@ private fun activeTint(
}
@BindingAdapter("data")
@Suppress("UNCHECKED_CAST")
fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
@@ -128,6 +129,7 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
}
@BindingAdapter("data")
@Suppress("UNCHECKED_CAST")
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
@@ -325,10 +327,10 @@ fun distance(meters: Number?): String? {
}
}
@InverseBindingAdapter(attribute = "app:values")
@InverseBindingAdapter(attribute = "values")
fun getRangeSliderValue(slider: RangeSlider) = slider.values
@BindingAdapter("app:valuesAttrChanged")
@BindingAdapter("valuesAttrChanged")
fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) {
slider.addOnChangeListener { _, _, _ ->
attrChange.onChange()
@@ -348,7 +350,7 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
return color
}
@BindingAdapter("app:tint")
@BindingAdapter("tint")
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}

View File

@@ -89,12 +89,12 @@ class RangeSliderPreference(context: Context, attrs: AttributeSet) : Preference(
slider.valueTo = valueTo
stepSize?.let { slider.stepSize = it }
slider.addOnChangeListener { slider, value, fromUser ->
slider.addOnChangeListener { slider, _, fromUser ->
if (fromUser && (updatesContinuously || !dragging)) {
syncValueInternal(slider)
}
}
slider.setOnTouchListener { v, event ->
slider.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> dragging = true
MotionEvent.ACTION_UP -> dragging = false

View File

@@ -16,7 +16,7 @@ import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.utils.distanceBetween
class FavoritesViewModel(application: Application, geApiKey: String) :
class FavoritesViewModel(application: Application) :
AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
@@ -69,7 +69,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
FavoritesListItem(
favorite,
totalAvailable(charger.id),
charger.chargepoints.sumBy { it.count },
charger.chargepoints.sumOf { it.count },
location.value.let { loc ->
if (loc == null) null else {
distanceBetween(

View File

@@ -92,4 +92,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
prefs.filterStatus = FILTERS_DISABLED
}
}
suspend fun resetValues() {
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
}
}

View File

@@ -7,6 +7,7 @@ import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.squareup.moshi.JsonDataException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -265,6 +266,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
} catch (e: AvailabilityDetectorException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: JsonDataException) {
// malformed JSON response from fronyx API
emit(Resource.error(e.message, null))
e.printStackTrace()
}
}
} ?: liveData { emit(Resource.success(null)) }

View File

@@ -13,7 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean
@Suppress("UNCHECKED_CAST")
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
override fun <T : ViewModel> create(modelClass: Class<T>): T = f() as T
}
@Suppress("UNCHECKED_CAST")
@@ -106,7 +106,8 @@ fun <T> throttleLatest(
}
}
public suspend fun <T> LiveData<T>.await(): T {
@ExperimentalCoroutinesApi
suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
@@ -124,7 +125,8 @@ public suspend fun <T> LiveData<T>.await(): T {
}
}
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
@ExperimentalCoroutinesApi
suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<Resource<T>> {
override fun onChanged(value: Resource<T>) {

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M10.83,8H21V6H8.83L10.83,8zM15.83,13H18v-2h-4.17L15.83,13zM14,16.83V18h-4v-2h3.17l-3,-3H6v-2h2.17l-3,-3H3V6h0.17L1.39,4.22l1.41,-1.41l18.38,18.38l-1.41,1.41L14,16.83z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z" />
</vector>

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_reset"
android:title="@string/menu_reset"
android:icon="@drawable/ic_filter_no"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_save_profile"
android:title="@string/menu_save_profile"

View File

@@ -8,9 +8,11 @@
<item
android:id="@+id/menu_edit_filters"
android:title="@string/menu_edit_filters"
android:menuCategory="secondary" />
android:menuCategory="secondary"
android:icon="@drawable/ic_edit" />
<item
android:id="@+id/menu_manage_filter_profiles"
android:title="@string/menu_manage_filter_profiles"
android:menuCategory="secondary" />
android:menuCategory="secondary"
android:icon="@drawable/ic_manage_filter_profiles" />
</menu>

View File

@@ -143,6 +143,7 @@
<string name="category_caravan_site">Wohnmobilstellplatz</string>
<string name="menu_apply">Filter anwenden</string>
<string name="menu_save_profile">Als Profil speichern</string>
<string name="menu_reset">Filter zurücksetzen</string>
<string name="no_filters">Keine Filter</string>
<string name="filter_custom">Verändertes Filterprofil</string>
<string name="filter_favorites">Favoriten</string>

View File

@@ -284,4 +284,16 @@
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
<string name="pref_prediction_enabled_summary">for støttede ladere
\n(foreløpig kun for likestrøm i Tyskland)</string>
<string name="chargeprice_price_not_available">Pris ikke tilgjengelig</string>
<string name="developer_mode_disabled">Utviklermodus avslått</string>
<string name="gps">GPS</string>
<string name="compass">Kompass</string>
<string name="pref_applink_associate">Åpne støttede lenker</string>
<string name="pref_applink_associate_summary">fra goingelectric.de og openchargemap.org</string>
<string name="chargeprice_header_other_tariffs">Andre ladeabonnementer</string>
<string name="disable_developer_mode">Skru av utviklermodus</string>
<string name="chargeprice_header_my_tariffs">Mine ladeabonnementer</string>
<string name="developer_options">Utvikleralternativer</string>
<string name="data_source_switched_to">Datakilde byttet til %s</string>
<string name="developer_mode_enabled">Utviklermodus påslått</string>
</resources>

View File

@@ -142,6 +142,7 @@
<string name="category_caravan_site">Caravan site</string>
<string name="menu_apply">Apply filters</string>
<string name="menu_save_profile">Save as profile</string>
<string name="menu_reset">Reset filter settings</string>
<string name="no_filters">No filters</string>
<string name="filter_custom">Modified filter</string>
<string name="filter_favorites">Favorites</string>

View File

@@ -9,7 +9,7 @@
(e.g. in the debug version). -->
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="${applicationId}"
android:targetPackage="net.vonforst.evmap"
android:targetClass="net.vonforst.evmap.MapsActivity">
<extra
android:name="favorites"

View File

@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.7.21'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.3'
repositories {
@@ -10,11 +10,10 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.android.tools.build:gradle:7.4.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"
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.4"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@@ -0,0 +1,7 @@
Verbesserungen:
- Neuer Knopf zum Zurücksetzen der Filtereinstellungen
- Filtermenü mit neuen Icons
- Übersetzungen aktualisiert
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,7 @@
Improvements:
- New button to reset filter setting
- Filter menu with new icons
- Updated translations
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crashes

View File

@@ -1,6 +1,6 @@
#Sat Aug 06 15:33:46 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME