Compare commits

..

15 Commits
0.1.2 ... 0.1.5

Author SHA1 Message Date
Johan von Forstner
2e9112f5c2 Release 0.1.5 2020-06-13 16:44:06 +02:00
Johan von Forstner
3c709fa3c5 add visual and haptic feedback when enabling/disabling filters 2020-06-13 16:19:50 +02:00
Johan von Forstner
11c868af66 remove TODO 2020-06-13 16:08:57 +02:00
Johan von Forstner
e3ea72bac6 implement new selection interface for network and chargecard filters 2020-06-13 16:03:52 +02:00
Johan von Forstner
d01371f6e9 add filters by network and charge card 2020-06-13 15:48:02 +02:00
Johan von Forstner
6130b190e1 disable/enable filters with long click on filter view 2020-06-13 08:04:06 +02:00
johan12345
128d156306 Release 0.1.4 2020-06-01 22:16:30 +02:00
johan12345
f855874d56 fix changed transition API 2020-06-01 22:08:56 +02:00
johan12345
92ebf6c1e5 update some libraries 2020-06-01 21:47:23 +02:00
Johan von Forstner
1e98be0f8f implement full display for opening hours (fixes #23) 2020-06-01 21:34:57 +02:00
Johan von Forstner
c0bec92d4c update Gradle plugin and Kotlin version 2020-06-01 16:35:25 +02:00
Johan von Forstner
71ecd492e9 show error dialog when Google Play Services are not available 2020-05-30 16:25:13 +02:00
Johan von Forstner
fcac8f91ad do not use white nav bar before Android API 27
(otherwise nav buttons are not visible)
2020-05-30 16:07:13 +02:00
johan12345
795c96d901 Release 0.1.3 2020-05-28 09:03:02 +02:00
johan12345
cc76310b2b fix string 2020-05-28 09:02:13 +02:00
29 changed files with 1026 additions and 107 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 10
versionName "0.1.2"
versionCode 13
versionName "0.1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -80,10 +80,10 @@ dependencies {
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.core:core:1.3.0-rc01'
implementation 'androidx.core:core:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'com.google.android.material:material:1.2.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
@@ -91,7 +91,7 @@ dependencies {
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.android.libraries.places:places:2.2.0'
implementation 'com.google.android.libraries.places:places:2.3.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
@@ -103,7 +103,7 @@ dependencies {
implementation 'io.michaelrocks:bimap:1.0.2'
// navigation library
def nav_version = "2.3.0-alpha06"
def nav_version = "2.3.0-beta01"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
@@ -119,7 +119,7 @@ dependencies {
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "2.2.0"
def billing_version = "2.2.1"
implementation "com.android.billingclient:billing:$billing_version"
implementation "com.android.billingclient:billing-ktx:$billing_version"

View File

@@ -3,6 +3,7 @@ package net.vonforst.evmap
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
@@ -12,11 +13,14 @@ import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.storage.PreferenceDataSource
const val REQUEST_LOCATION_PERMISSION = 1
class MapsActivity : AppCompatActivity() {
@@ -47,6 +51,8 @@ class MapsActivity : AppCompatActivity() {
findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
prefs = PreferenceDataSource(this)
checkPlayServices()
}
fun navigateTo(charger: ChargeLocation) {
@@ -92,4 +98,19 @@ class MapsActivity : AppCompatActivity() {
}
startActivity(intent)
}
private fun checkPlayServices(): Boolean {
val request = 9000
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
if (resultCode != ConnectionResult.SUCCESS) {
if (apiAvailability.isUserResolvableError(resultCode)) {
apiAvailability.getErrorDialog(this, resultCode, request).show()
} else {
Log.d("EVMap", "This device is not supported.")
}
return false
}
return true
}
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.databinding.DataBindingUtil
import androidx.databinding.Observable
@@ -17,8 +18,11 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
import net.vonforst.evmap.fragment.MultiSelectDialog
import net.vonforst.evmap.viewmodel.*
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -28,8 +32,8 @@ interface Equatable {
override fun equals(other: Any?): Boolean;
}
abstract class DataBindingAdapter<T : Equatable>() :
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback()) {
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
var onClickListener: ((T) -> Unit)? = null
@@ -50,14 +54,20 @@ abstract class DataBindingAdapter<T : Equatable>() :
open fun bind(holder: ViewHolder<T>, item: T) {
holder.binding.setVariable(BR.item, item)
holder.binding.executePendingBindings()
holder.binding.root.setOnClickListener {
val listener = onClickListener ?: return@setOnClickListener
listener(item)
if (onClickListener != null) {
holder.binding.root.setOnClickListener {
val listener = onClickListener ?: return@setOnClickListener
listener(item)
}
}
}
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem === newItem
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = if (getKey != null) {
(getKey)(oldItem) == (getKey)(newItem)
} else {
oldItem === newItem
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
}
@@ -90,10 +100,18 @@ class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
val text: CharSequence,
val detailText: CharSequence? = null,
val links: Boolean = true,
val clickable: Boolean = false
val clickable: Boolean = false,
val hoursDays: OpeningHoursDays? = null
) : Equatable
override fun getItemViewType(position: Int): Int = R.layout.item_detail
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if (item.hoursDays != null) {
return R.layout.item_detail_openinghours
} else {
return R.layout.item_detail
}
}
}
fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail> {
@@ -128,12 +146,12 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
loc.faultReport.description ?: "",
clickable = true
) else null,
// TODO: separate layout for opening hours with expandable details
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
loc.openinghours.getStatusText(ctx),
loc.openinghours.description
loc.openinghours.description,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailAdapter.Detail(
R.drawable.ic_cost,
@@ -173,11 +191,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
val itemids = mutableMapOf<String, Long>()
var maxId = 0L
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
is BooleanFilter -> R.layout.item_filter_boolean
is MultipleChoiceFilter -> R.layout.item_filter_multiple_choice
is SliderFilter -> R.layout.item_filter_slider
}
override fun getItemViewType(position: Int): Int =
when (val filter = getItem(position).filter) {
is BooleanFilter -> R.layout.item_filter_boolean
is MultipleChoiceFilter -> {
if (filter.manyChoices) {
R.layout.item_filter_multiple_choice_large
} else {
R.layout.item_filter_multiple_choice
}
}
is SliderFilter -> R.layout.item_filter_slider
}
override fun bind(
holder: ViewHolder<FilterWithValue<FilterValue>>,
@@ -192,10 +217,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
)
}
is MultipleChoiceFilterValue -> {
setupMultipleChoice(
holder.binding as ItemFilterMultipleChoiceBinding,
item.filter as MultipleChoiceFilter, item.value
)
val filter = item.filter as MultipleChoiceFilter
if (filter.manyChoices) {
setupMultipleChoiceMany(
holder.binding as ItemFilterMultipleChoiceLargeBinding,
filter, item.value
)
} else {
setupMultipleChoice(
holder.binding as ItemFilterMultipleChoiceBinding,
filter, item.value
)
}
}
}
}
@@ -283,6 +316,27 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
updateButtons()
}
private fun setupMultipleChoiceMany(
binding: ItemFilterMultipleChoiceLargeBinding,
filter: MultipleChoiceFilter,
value: MultipleChoiceFilterValue
) {
if (value.all) {
value.values = filter.choices.keys.toMutableSet()
binding.notifyPropertyChanged(BR.item)
}
binding.btnEdit.setOnClickListener {
val dialog = MultiSelectDialog.getInstance(filter.name, filter.choices, value.values)
dialog.okListener = { selected ->
value.values = selected.toMutableSet()
value.all = value.values == filter.choices.keys
binding.item = binding.item
}
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
}
}
private fun setupSlider(
binding: ItemFilterSliderBinding,
filter: SliderFilter,

View File

@@ -23,7 +23,9 @@ interface GoingElectricApi {
@Query("freecharging") freecharging: Boolean,
@Query("freeparking") freeparking: Boolean,
@Query("min_power") minPower: Int,
@Query("plugs") plugs: String?
@Query("plugs") plugs: String?,
@Query("chargecards") chargecards: String?,
@Query("networks") networks: String?
): Response<ChargepointList>
@GET("chargepoints/")
@@ -36,7 +38,7 @@ interface GoingElectricApi {
suspend fun getNetworks(): Response<StringList>
@GET("chargepoints/chargecardlist/")
suspend fun getChargeCards(): Response<StringList>
suspend fun getChargeCards(): Response<ChargeCardList>
companion object {
private val cacheSize = 10L * 1024 * 1024; // 10MB

View File

@@ -15,6 +15,8 @@ import java.time.DayOfWeek
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.math.abs
import kotlin.math.floor
@@ -31,6 +33,12 @@ data class StringList(
val result: List<String>
)
@JsonClass(generateAdapter = true)
data class ChargeCardList(
val status: String,
val result: List<ChargeCard>
)
sealed class ChargepointListItem
@JsonClass(generateAdapter = true)
@@ -158,9 +166,12 @@ data class OpeningHoursDays(
) {
fun getHoursForDate(date: LocalDate): Hours {
// TODO: check for holidays
return getHoursForDayOfWeek(date.dayOfWeek)
}
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
return when (date.dayOfWeek) {
return when (dayOfWeek) {
DayOfWeek.MONDAY -> monday
DayOfWeek.TUESDAY -> tuesday
DayOfWeek.WEDNESDAY -> wednesday
@@ -168,6 +179,7 @@ data class OpeningHoursDays(
DayOfWeek.FRIDAY -> friday
DayOfWeek.SATURDAY -> saturday
DayOfWeek.SUNDAY -> sunday
null -> holiday
}
}
}
@@ -175,7 +187,16 @@ data class OpeningHoursDays(
data class Hours(
val start: LocalTime?,
val end: LocalTime?
)
) {
override fun toString(): String {
if (start != null && end != null) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
} else {
return "closed"
}
}
}
@JsonClass(generateAdapter = true)
@Parcelize
@@ -248,4 +269,12 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
}
@JsonClass(generateAdapter = true)
data class FaultReport(val created: Instant?, val description: String?)
data class FaultReport(val created: Instant?, val description: String?)
@Entity
@JsonClass(generateAdapter = true)
data class ChargeCard(
@Json(name = "card_id") @PrimaryKey val id: Long,
val name: String,
val url: String
)

View File

@@ -10,7 +10,6 @@ import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.transition.TransitionManager
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
@@ -32,6 +31,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
@@ -42,6 +42,7 @@ import com.google.android.gms.maps.model.*
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 com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
@@ -250,7 +251,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val materialTransform = MaterialContainerTransform().apply {
startView = binding.fabLayers
endView = binding.layersSheet
pathMotion = MaterialArcMotion()
setPathMotion(MaterialArcMotion())
duration = 250
scrimColor = Color.TRANSPARENT
}
@@ -263,7 +264,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val materialTransform = MaterialContainerTransform().apply {
startView = binding.layersSheet
endView = binding.fabLayers
pathMotion = MaterialArcMotion()
setPathMotion(MaterialArcMotion())
duration = 200
scrimColor = Color.TRANSPARENT
}
@@ -691,9 +692,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
filterView?.setOnLongClickListener {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
// enable/disable filters
vm.filtersActive.value = !vm.filtersActive.value!!
// haptic feedback
filterView.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
)
// show snackbar
Snackbar.make(
requireView(), if (vm.filtersActive.value!!) {
R.string.filters_activated
} else {
R.string.filters_deactivated
}, Snackbar.LENGTH_SHORT
).show()
true
}
}

View File

@@ -0,0 +1,114 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.dialog_multi_select.*
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.Equatable
import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
class MultiSelectDialog : AppCompatDialogFragment() {
companion object {
fun getInstance(
title: String,
data: Map<String, String>,
selected: Set<String>
): MultiSelectDialog {
val dialog = MultiSelectDialog()
dialog.arguments = Bundle().apply {
putString("title", title)
putSerializable("data", HashMap(data))
putSerializable("selected", HashSet(selected))
}
return dialog
}
}
var okListener: ((Set<String>) -> Unit)? = null
var cancelListener: (() -> Unit)? = null
private lateinit var items: List<MultiSelectItem>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_multi_select, container)
}
override fun onStart() {
super.onStart()
// dialog with 95% screen height
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
(resources.displayMetrics.heightPixels * 0.95).toInt()
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val data = requireArguments().getSerializable("data") as HashMap<String, String>
val selected = requireArguments().getSerializable("selected") as HashSet<String>
val title = requireArguments().getString("title")
dialogTitle.text = title
val adapter = Adapter()
list.adapter = adapter
list.layoutManager = LinearLayoutManager(view.context)
items = data.entries.toList().sortedBy { it.key }.map {
MultiSelectItem(it.key, it.value, it.key in selected)
}
adapter.submitList(items)
etSearch.doAfterTextChanged { text ->
adapter.submitList(search(items, text.toString()))
}
btnCancel.setOnClickListener {
cancelListener?.let { listener ->
listener()
}
dismiss()
}
btnOK.setOnClickListener {
okListener?.let { listener ->
val result = items.filter { it.selected }.map { it.key }.toSet()
listener(result)
}
dismiss()
}
btnAll.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, true) }
adapter.submitList(search(items, etSearch.text.toString()))
}
btnNone.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, false) }
adapter.submitList(search(items, etSearch.text.toString()))
}
}
private fun search(
items: List<MultiSelectItem>,
text: String
): List<MultiSelectItem> {
return items.filter { item ->
// search for string within name
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
}
}
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
}
}
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable

View File

@@ -0,0 +1,47 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.time.Duration
import java.time.Instant
@Dao
interface ChargeCardDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg chargeCards: ChargeCard)
@Delete
suspend fun delete(vararg chargeCards: ChargeCard)
@Query("SELECT * FROM chargeCard")
fun getAllChargeCards(): LiveData<List<ChargeCard>>
}
class ChargeCardRepository(
private val api: GoingElectricApi, private val scope: CoroutineScope,
private val dao: ChargeCardDao, private val prefs: PreferenceDataSource
) {
fun getChargeCards(): LiveData<List<ChargeCard>> {
scope.launch {
updateChargeCards()
}
return dao.getAllChargeCards()
}
private suspend fun updateChargeCards() {
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getChargeCards()
if (!response.isSuccessful) return
for (card in response.body()!!.result) {
dao.insert(card)
}
prefs.lastChargeCardUpdate = Instant.now()
}
}

View File

@@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
@@ -18,20 +19,27 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
BooleanFilterValue::class,
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
Plug::class
], version = 6
Plug::class,
Network::class,
ChargeCard::class
], version = 7
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun filterValueDao(): FilterValueDao
abstract fun plugDao(): PlugDao
abstract fun networkDao(): NetworkDao
abstract fun chargeCardDao(): ChargeCardDao
companion object {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6)
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7
)
.build()
}
@@ -99,5 +107,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_7 = object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `Network` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `ChargeCard` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
}
}

View File

@@ -0,0 +1,49 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.time.Duration
import java.time.Instant
@Entity
data class Network(@PrimaryKey val name: String)
@Dao
interface NetworkDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg networks: Network)
@Delete
suspend fun delete(vararg networks: Network)
@Query("SELECT * FROM network")
fun getAllNetworks(): LiveData<List<Network>>
}
class NetworkRepository(
private val api: GoingElectricApi, private val scope: CoroutineScope,
private val dao: NetworkDao, private val prefs: PreferenceDataSource
) {
fun getNetworks(): LiveData<List<Network>> {
scope.launch {
updateNetworks()
}
return dao.getAllNetworks()
}
private suspend fun updateNetworks() {
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getNetworks()
if (!response.isSuccessful) return
for (name in response.body()!!.result) {
dao.insert(Network(name))
}
prefs.lastNetworkUpdate = Instant.now()
}
}

View File

@@ -18,4 +18,16 @@ class PreferenceDataSource(context: Context) {
set(value) {
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
}
var lastNetworkUpdate: Instant
get() = Instant.ofEpochMilli(sp.getLong("last_network_update", 0L))
set(value) {
sp.edit().putLong("last_network_update", value.toEpochMilli()).apply()
}
var lastChargeCardUpdate: Instant
get() = Instant.ofEpochMilli(sp.getLong("last_chargecard_update", 0L))
set(value) {
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
}
}

View File

@@ -2,9 +2,6 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.view.View
import android.widget.ImageView
import android.widget.TextView

View File

@@ -10,12 +10,10 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.Plug
import net.vonforst.evmap.storage.PlugRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.*
import kotlin.math.abs
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
@@ -28,7 +26,9 @@ internal fun mapPowerInverse(power: Int) = powerSteps
internal fun getFilters(
application: Application,
plugs: LiveData<List<Plug>>
plugs: LiveData<List<Plug>>,
networks: LiveData<List<Network>>,
chargeCards: LiveData<List<ChargeCard>>
): LiveData<List<Filter<FilterValue>>> {
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
val plugNames = mapOf(
@@ -42,35 +42,57 @@ internal fun getFilters(
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
)
addSource(plugs) { plugs ->
val plugMap = plugs.map { plug ->
plug.name to (plugNames[plug.name] ?: plug.name)
}.toMap()
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
SliderFilter(
application.getString(R.string.filter_min_power), "min_power",
powerSteps.size - 1,
mapping = ::mapPower,
inverseMapping = ::mapPowerInverse,
unit = "kW"
),
MultipleChoiceFilter(
application.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
),
SliderFilter(
application.getString(R.string.filter_min_connectors),
"min_connectors",
10
)
)
listOf(plugs, networks, chargeCards).forEach { source ->
addSource(source) { _ ->
buildFilters(plugs, plugNames, networks, chargeCards, application)
}
}
}
}
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
plugs: LiveData<List<Plug>>,
plugNames: Map<String, String>,
networks: LiveData<List<Network>>,
chargeCards: LiveData<List<ChargeCard>>,
application: Application
) {
val plugMap = plugs.value?.map { plug ->
plug.name to (plugNames[plug.name] ?: plug.name)
}?.toMap() ?: return
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.name to it.name }?.toMap() ?: return
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
SliderFilter(
application.getString(R.string.filter_min_power), "min_power",
powerSteps.size - 1,
mapping = ::mapPower,
inverseMapping = ::mapPowerInverse,
unit = "kW"
),
MultipleChoiceFilter(
application.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
),
SliderFilter(
application.getString(R.string.filter_min_connectors),
"min_connectors",
10
),
MultipleChoiceFilter(
application.getString(R.string.filter_networks), "networks",
networkMap, manyChoices = true
),
MultipleChoiceFilter(
application.getString(R.string.filter_chargecards), "chargecards",
chargecardMap, manyChoices = true
)
)
}
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
@@ -107,9 +129,14 @@ class FilterViewModel(application: Application, geApiKey: String) :
private val plugs: LiveData<List<Plug>> by lazy {
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
}
private val networks: LiveData<List<Network>> by lazy {
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
}
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
}
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
getFilters(application, plugs)
getFilters(application, plugs, networks, chargeCards)
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
@@ -144,7 +171,8 @@ data class MultipleChoiceFilter(
override val name: String,
override val key: String,
val choices: Map<String, String>,
val commonChoices: Set<String>? = null
val commonChoices: Set<String>? = null,
val manyChoices: Boolean = false
) : Filter<MultipleChoiceFilterValue>() {
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)

View File

@@ -9,14 +9,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.ChargepointList
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.Plug
import net.vonforst.evmap.storage.PlugRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.storage.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@@ -52,7 +46,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private val plugs: LiveData<List<Plug>> by lazy {
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
}
private val filters = getFilters(application, plugs)
private val networks: LiveData<List<Network>> by lazy {
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
}
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
}
private val filters = getFilters(application, plugs, networks, chargeCards)
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
filtersWithValue(filters, filterValues, filtersActive)
@@ -191,22 +191,31 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
zoom: Float,
filters: List<FilterWithValue<out FilterValue>>
): Resource<List<ChargepointListItem>> {
val freecharging =
(filters.find { it.value.key == "freecharging" }!!.value as BooleanFilterValue).value
val freeparking =
(filters.find { it.value.key == "freeparking" }!!.value as BooleanFilterValue).value
val minPower =
(filters.find { it.value.key == "min_power" }!!.value as SliderFilterValue).value
val minConnectors =
(filters.find { it.value.key == "min_connectors" }!!.value as SliderFilterValue).value
val freecharging = getBooleanValue(filters, "freecharging")
val freeparking = getBooleanValue(filters, "freeparking")
val minPower = getSliderValue(filters, "min_power")
val minConnectors = getSliderValue(filters, "min_connectors")
val connectorsVal =
filters.find { it.value.key == "connectors" }!!.value as MultipleChoiceFilterValue
val connectors = if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
val connectors = formatMultipleChoice(connectorsVal)
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
// no chargeCards chosen
return Resource.success(emptyList())
}
val chargeCards = formatMultipleChoice(chargeCardsVal)
val networksVal = getMultipleChoiceValue(filters, "networks")
if (networksVal.values.isEmpty() && !networksVal.all) {
// no networks chosen
return Resource.success(emptyList())
}
val networks = formatMultipleChoice(networksVal)
// do not use clustering if filters need to be applied locally.
val useClustering = minConnectors <= 1 && zoom < 13
@@ -217,7 +226,8 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = useClustering, zoom = zoom,
clusterDistance = clusterDistance, freecharging = freecharging, minPower = minPower,
freeparking = freeparking, plugs = connectors
freeparking = freeparking, plugs = connectors, chargecards = chargeCards,
networks = networks
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
@@ -239,6 +249,24 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
private fun getBooleanValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
private fun getSliderValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
private fun getMultipleChoiceValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?selectableItemBackgroundBorderless"/>
<item android:drawable="@drawable/expand_toggle_icon"
android:top="4dp"
android:left="4dp"
android:right="4dp"
android:bottom="4dp"/>
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" android:drawable="@drawable/ic_expand" />
<item android:state_checked="true" android:drawable="@drawable/ic_collapse" />
</selector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

@@ -0,0 +1,92 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialogTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/btnAll"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Select Something" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/btnOK"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tilSearch" />
<Button
android:id="@+id/btnAll"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all"
app:layout_constraintBaseline_toBaselineOf="@+id/dialogTitle"
app:layout_constraintEnd_toStartOf="@+id/btnNone" />
<Button
android:id="@+id/btnOK"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="@string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnCancel"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnOK" />
<Button
android:id="@+id/btnNone"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/none"
app:layout_constraintBaseline_toBaselineOf="@+id/btnAll"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSearch"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:hint="@string/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnAll"
app:startIconDrawable="@drawable/ic_search">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.fragment.MultiSelectItem" />
<variable
name="item"
type="MultiSelectItem" />
</data>
<CheckBox
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:layout_marginStart="?attr/dialogPreferredPadding"
android:layout_marginEnd="?attr/dialogPreferredPadding"
android:paddingStart="20dp"
android:ellipsize="marquee"
android:text="@{item.name}"
android:checked="@={item.selected}"
tools:text="Item"
tools:ignore="RtlSymmetry" />
</layout>

View File

@@ -0,0 +1,204 @@
<?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="java.time.DayOfWeek" />
<import type="android.transition.TransitionManager" />
<variable
name="item"
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="@{item.clickable}"
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="14dp"
android:maxLines="1"
android:text="@{item.text}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
app:layout_constraintStart_toEndOf="@+id/imageView3"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:text="Lorem ipsum" />
<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@{item.contentDescription}"
android:tint="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/textView8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:linksClickable="@{item.links}"
android:text="@{item.detailText}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9"
app:layout_constraintVertical_bias="0.0"
tools:text="Lorem ipsum" />
<include
android:id="@+id/hours_mon"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:hours="@{item.hoursDays}"
app:dayOfWeek="@{DayOfWeek.MONDAY}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
<include
android:id="@+id/hours_tue"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
<include
android:id="@+id/hours_wed"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
<include
android:id="@+id/hours_thu"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
<include
android:id="@+id/hours_fri"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
<include
android:id="@+id/hours_sat"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
<include
android:id="@+id/hours_sun"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
<include
android:id="@+id/hours_holiday"
layout="@layout/item_detail_openinghours_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:goneUnless="@{expandToggle.checked}"
app:dayOfWeek="@{null}"
app:hours="@{item.hoursDays}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
<ToggleButton
android:id="@+id/expandToggle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/expand_toggle"
android:textOff=""
android:textOn=""
android:onClick="@{() -> TransitionManager.beginDelayedTransition(container)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="java.time.format.TextStyle" />
<import type="java.util.Locale" />
<variable
name="hours"
type="net.vonforst.evmap.api.goingelectric.OpeningHoursDays" />
<variable
name="dayOfWeek"
type="java.time.DayOfWeek" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView24"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{dayOfWeek != null ? dayOfWeek.getDisplayName(TextStyle.FULL, context.resources.configuration.locale) : @string/holiday}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Montag" />
<TextView
android:id="@+id/textView25"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals(&quot;closed&quot;) ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="07:00-21:00" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,70 @@
<?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="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
<variable
name="item"
type="FilterWithValue&lt;MultipleChoiceFilterValue&gt;" />
<variable
name="showingAll"
type="boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView17"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Networks" />
<ImageButton
android:id="@+id/btnEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_edit"
android:contentDescription="@string/edit" />
<TextView
android:id="@+id/textView26"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView17"
tools:text="4 selected" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -10,6 +10,8 @@
<string name="hours">Öffnungszeiten</string>
<string name="open_247"><![CDATA[<b>24 Stunden geöffnet</b>]]></string>
<string name="closed"><![CDATA[<b>Geschlossen</b>]]></string>
<string name="closed_unfmt">Geschlossen</string>
<string name="holiday">Feiertag</string>
<string name="open_closesat"><![CDATA[<b>Geöffnet</b> · Schließt um %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
<string name="cost">Kosten</string>
@@ -75,8 +77,17 @@
<string name="map_traffic">Verkehr</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filter aktiv</string>
<string name="filters_activated">Filter aktiviert</string>
<string name="filters_deactivated">Filter deaktiviert</string>
<string name="menu_edit_filters">Filter bearbeiten…</string>
<string name="go_to_chargeprice"><![CDATA[Preisvergleich<br/><small>mit Chargeprice.app</small>]]></string>
<string name="go_to_chargeprice">Preisvergleich</string>
<string name="fault_report">Störungsmeldung</string>
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
<string name="filter_networks">Verbünde</string>
<string name="filter_chargecards">Ladetarife</string>
<string name="all_selected">Alle ausgewählt</string>
<string name="number_selected">%d ausgewählt</string>
<string name="edit">bearbeiten</string>
<string name="cancel">Abbrechen</string>
<string name="ok">OK</string>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="AppTheme.Base">
<item name="android:navigationBarColor">@android:color/white</item>
</style>
</resources>

View File

@@ -11,6 +11,8 @@
<string name="closed"><![CDATA[<b>Closed</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Open</b> · Closes at %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Closed</b> · Opens at %s]]></string>
<string name="closed_unfmt">Closed</string>
<string name="holiday">Holiday</string>
<string name="cost">Cost</string>
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
<string name="free">Free</string>
@@ -74,8 +76,17 @@
<string name="map_traffic">Traffic</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filters active</string>
<string name="filters_activated">Filters activated</string>
<string name="filters_deactivated">Filters deactivated</string>
<string name="menu_edit_filters">Edit filters…</string>
<string name="go_to_chargeprice">Compare prices</string>
<string name="fault_report">Fault report</string>
<string name="fault_report_date">Fault report (last update: %s)</string>
<string name="filter_networks">Networks</string>
<string name="filter_chargecards">Payment methods</string>
<string name="all_selected">All selected</string>
<string name="number_selected">%d selected</string>
<string name="edit">edit</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
</resources>

View File

@@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.71'
ext.kotlin_version = '1.3.72'
ext.about_libs_version = '8.1.1'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0-rc01'
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"