From 5c72ee718b902ac45cb79ec2f61bb3003c496e4a Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Tue, 28 Apr 2020 19:38:10 +0200 Subject: [PATCH] working implementation for first filter (free charging) #9 --- .../evmap/adapter/DataBindingAdapters.kt | 11 +-- .../api/goingelectric/GoingElectricApi.kt | 3 +- .../vonforst/evmap/fragment/FilterFragment.kt | 24 ++++-- .../net/vonforst/evmap/storage/Database.kt | 24 +++++- .../vonforst/evmap/storage/FilterValueDao.kt | 43 +++++++++++ .../vonforst/evmap/storage/TypeConverters.kt | 14 ++++ .../evmap/viewmodel/FilterViewModel.kt | 74 +++++++++++++++++-- .../vonforst/evmap/viewmodel/MapViewModel.kt | 41 +++++++--- app/src/main/res/drawable/ic_check.xml | 10 +++ app/src/main/res/layout/fragment_filter.xml | 2 +- .../main/res/layout/item_filter_boolean.xml | 9 ++- app/src/main/res/menu/filter.xml | 9 +++ 12 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt create mode 100644 app/src/main/res/drawable/ic_check.xml create mode 100644 app/src/main/res/menu/filter.xml diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index a0ba5ec7..fb815ecc 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -13,10 +13,7 @@ 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.viewmodel.BooleanFilter -import net.vonforst.evmap.viewmodel.FavoritesViewModel -import net.vonforst.evmap.viewmodel.Filter -import net.vonforst.evmap.viewmodel.MultipleChoiceFilter +import net.vonforst.evmap.viewmodel.* interface Equatable { override fun equals(other: Any?): Boolean; @@ -137,7 +134,7 @@ class FavoritesAdapter(val vm: FavoritesViewModel) : override fun getItemId(position: Int): Long = getItem(position).charger.id } -class FiltersAdapter : DataBindingAdapter() { +class FiltersAdapter : DataBindingAdapter>() { init { setHasStableIds(true) } @@ -145,13 +142,13 @@ class FiltersAdapter : DataBindingAdapter() { val itemids = mutableMapOf() var maxId = 0L - override fun getItemViewType(position: Int): Int = when (getItem(position)) { + override fun getItemViewType(position: Int): Int = when (getItem(position).filter) { is BooleanFilter -> R.layout.item_filter_boolean is MultipleChoiceFilter -> R.layout.item_filter_boolean } override fun getItemId(position: Int): Long { - val key = getItem(position).key + val key = getItem(position).filter.key var value = itemids[key] if (value == null) { maxId++ diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt index 2be82533..ce306fbe 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt @@ -16,7 +16,8 @@ interface GoingElectricApi { @Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double, @Query("clustering") clustering: Boolean, @Query("zoom") zoom: Float, - @Query("cluster_distance") clusterDistance: Int + @Query("cluster_distance") clusterDistance: Int, + @Query("freecharging") freecharging: Boolean ): Call @GET("chargepoints/") diff --git a/app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt index 535699e4..664e5609 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/FilterFragment.kt @@ -1,19 +1,19 @@ package net.vonforst.evmap.fragment import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.launch import net.vonforst.evmap.MapsActivity import net.vonforst.evmap.R import net.vonforst.evmap.adapter.FiltersAdapter @@ -43,6 +43,7 @@ class FilterFragment : Fragment() { binding.lifecycleOwner = this binding.vm = vm + setHasOptionsMenu(true) requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -55,6 +56,7 @@ class FilterFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val toolbar = view.findViewById(R.id.toolbar) as Toolbar + (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar) val navController = findNavController() toolbar.setupWithNavController( @@ -74,11 +76,23 @@ class FilterFragment : Fragment() { } view.startCircularReveal() + + toolbar.setNavigationOnClickListener { + exitAfterTransition() + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.filter, menu) + super.onCreateOptionsMenu(menu, inflater) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - android.R.id.home -> { + R.id.menu_apply -> { + lifecycleScope.launch { + vm.saveFilterValues() + } exitAfterTransition() true } diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt index bc17cb33..73a7271a 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -5,22 +5,42 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.viewmodel.BooleanFilterValue +import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue -@Database(entities = [ChargeLocation::class], version = 1) +@Database( + entities = [ + ChargeLocation::class, + BooleanFilterValue::class, + MultipleChoiceFilterValue::class + ], version = 2 +) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun chargeLocationsDao(): ChargeLocationsDao + abstract fun filterValueDao(): FilterValueDao companion object { private lateinit var context: Context private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db").build() + Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db") + .addMigrations(MIGRATION_2) + .build() } fun getInstance(context: Context): AppDatabase { this.context = context.applicationContext return database } + + private val MIGRATION_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `BooleanFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))") + db.execSQL("CREATE TABLE IF NOT EXISTS `MultipleChoiceFilterValue` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, PRIMARY KEY(`key`))") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt b/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt new file mode 100644 index 00000000..2ad3edad --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt @@ -0,0 +1,43 @@ +package net.vonforst.evmap.storage + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.room.* +import net.vonforst.evmap.viewmodel.BooleanFilterValue +import net.vonforst.evmap.viewmodel.FilterValue +import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue + +@Dao +abstract class FilterValueDao { + @Query("SELECT * FROM booleanfiltervalue") + protected abstract fun getBooleanFilterValues(): LiveData> + + @Query("SELECT * FROM multiplechoicefiltervalue") + protected abstract fun getMultipleChoiceFilterValues(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract suspend fun insert(vararg values: BooleanFilterValue) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract suspend fun insert(vararg values: MultipleChoiceFilterValue) + + open fun getFilterValues(): LiveData> = + MediatorLiveData>().apply { + val sources = listOf(getBooleanFilterValues(), getMultipleChoiceFilterValues()) + for (source in sources) { + addSource(source) { + value = sources.mapNotNull { it.value }.flatten() + } + } + } + + @Transaction + open suspend fun insert(vararg values: FilterValue) { + values.forEach { + when (it) { + is BooleanFilterValue -> insert(it) + is MultipleChoiceFilterValue -> insert(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt index e416d095..22cb046c 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt @@ -17,6 +17,10 @@ class Converters { val type = Types.newParameterizedType(List::class.java, ChargerPhoto::class.java) moshi.adapter>(type) } + private val stringSetAdapter by lazy { + val type = Types.newParameterizedType(Set::class.java, String::class.java) + moshi.adapter>(type) + } @TypeConverter fun fromChargepointList(value: List?): String { @@ -49,4 +53,14 @@ class Converters { LocalTime.parse(it) } } + + @TypeConverter + fun fromStringSet(value: Set?): String { + return stringSetAdapter.toJson(value) + } + + @TypeConverter + fun toStringSet(value: String): Set? { + return stringSetAdapter.fromJson(value) + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt index 8851a34d..ebeeecf5 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt @@ -2,35 +2,99 @@ package net.vonforst.evmap.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +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.GoingElectricApi import net.vonforst.evmap.storage.AppDatabase +import kotlin.reflect.KClass +import kotlin.reflect.full.cast class FilterViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) { private var api = GoingElectricApi.create(geApiKey) private var db = AppDatabase.getInstance(application) - val filters: MutableLiveData> by lazy { - MutableLiveData>().apply { + private val filters: MutableLiveData>> by lazy { + MutableLiveData>>().apply { value = listOf( BooleanFilter(application.getString(R.string.filter_free), "freecharging") ) } } + + private val filterValues: LiveData> by lazy { + db.filterValueDao().getFilterValues() + } + + val filtersWithValue: LiveData>> by lazy { + MediatorLiveData>>().apply { + for (source in listOf(filters, filterValues)) { + addSource(source) { + val filters = filters.value + val values = filterValues.value + if (filters != null && values != null) { + value = filters.map { filter -> + val value = + values.find { it.key == filter.key } ?: filter.defaultValue() + FilterWithValue(filter, filter.valueClass.cast(value)) + } + } else { + value = null + } + } + } + } + } + + suspend fun saveFilterValues() { + filtersWithValue.value?.forEach { + db.filterValueDao().insert(it.value) + } + } } -sealed class Filter : Equatable { +sealed class Filter : Equatable { abstract val name: String abstract val key: String + abstract val valueClass: KClass + abstract fun defaultValue(): T } -data class BooleanFilter(override val name: String, override val key: String) : Filter() +data class BooleanFilter(override val name: String, override val key: String) : + Filter() { + override val valueClass: KClass = BooleanFilterValue::class + override fun defaultValue() = BooleanFilterValue(key, false) +} data class MultipleChoiceFilter( override val name: String, override val key: String, val choices: Map -) : Filter() \ No newline at end of file +) : Filter() { + override val valueClass: KClass = MultipleChoiceFilterValue::class + override fun defaultValue() = MultipleChoiceFilterValue(key, emptySet(), true) +} + +sealed class FilterValue : Equatable { + abstract val key: String +} + +@Entity +data class BooleanFilterValue( + @PrimaryKey override val key: String, + var value: Boolean +) : FilterValue() + +@Entity +data class MultipleChoiceFilterValue( + @PrimaryKey override val key: String, + val values: Set, + val all: Boolean +) : FilterValue() + +data class FilterWithValue(val filter: Filter, val value: T) : Equatable \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index c73f6926..92e517f4 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -28,12 +28,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode val mapPosition: MutableLiveData by lazy { MutableLiveData() } + private val filterValues: LiveData> by lazy { + db.filterValueDao().getFilterValues() + } val chargepoints: MediatorLiveData>> by lazy { MediatorLiveData>>() .apply { value = Resource.loading(emptyList()) - addSource(mapPosition) { - mapPosition.value?.let { pos -> loadChargepoints(pos) } + listOf(mapPosition, filterValues).forEach { + addSource(it) { + val pos = mapPosition.value ?: return@addSource + val filterValues = filterValues.value ?: return@addSource + loadChargepoints(pos, filterValues) + } } } } @@ -97,16 +104,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode } } - private fun loadChargepoints(mapPosition: MapPosition) { + private fun loadChargepoints(mapPosition: MapPosition, filterValues: List) { chargepoints.value = Resource.loading(chargepoints.value?.data) val bounds = mapPosition.bounds val zoom = mapPosition.zoom - api.getChargepoints( - bounds.southwest.latitude, bounds.southwest.longitude, - bounds.northeast.latitude, bounds.northeast.longitude, - clustering = zoom < 13, zoom = zoom, - clusterDistance = 70 - ).enqueue(object : Callback { + getChargepointsWithFilters(bounds, zoom, filterValues).enqueue(object : + Callback { override fun onFailure(call: Call, t: Throwable) { chargepoints.value = Resource.error(t.message, chargepoints.value?.data) t.printStackTrace() @@ -126,6 +129,26 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode }) } + private fun getChargepointsWithFilters( + bounds: LatLngBounds, + zoom: Float, + filterValues: List + ): Call { + var freecharging = false + filterValues.forEach { + when (it.key) { + "freecharging" -> freecharging = (it as BooleanFilterValue).value + } + } + + return api.getChargepoints( + bounds.southwest.latitude, bounds.southwest.longitude, + bounds.northeast.latitude, bounds.northeast.longitude, + clustering = zoom < 13, zoom = zoom, + clusterDistance = 70, freecharging = freecharging + ) + } + private suspend fun loadAvailability(charger: ChargeLocation) { availability.value = Resource.loading(null) availability.value = getAvailability(charger) diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000..9fadddff --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_filter.xml b/app/src/main/res/layout/fragment_filter.xml index 86c23be8..ea377c5e 100644 --- a/app/src/main/res/layout/fragment_filter.xml +++ b/app/src/main/res/layout/fragment_filter.xml @@ -33,6 +33,6 @@ android:id="@+id/filters_list" android:layout_width="match_parent" android:layout_height="match_parent" - app:data="@{vm.filters}" /> + app:data="@{vm.filtersWithValue}" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_filter_boolean.xml b/app/src/main/res/layout/item_filter_boolean.xml index b4b02b47..3cf41b97 100644 --- a/app/src/main/res/layout/item_filter_boolean.xml +++ b/app/src/main/res/layout/item_filter_boolean.xml @@ -7,9 +7,13 @@ + + + + + type="FilterWithValue<BooleanFilterValue>" /> diff --git a/app/src/main/res/menu/filter.xml b/app/src/main/res/menu/filter.xml new file mode 100644 index 00000000..aa9b46f1 --- /dev/null +++ b/app/src/main/res/menu/filter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file