From d5e29a5112ad38dcd3f2290efe46fb3940901dcb Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Thu, 29 Jul 2021 18:05:03 +0200 Subject: [PATCH] Android Auto: implement filter profiles fixes #72 --- .../evmap/auto/ChargerDetailScreen.kt | 41 ++----- .../net/vonforst/evmap/auto/FilterScreen.kt | 90 +++++++++++++++ .../java/net/vonforst/evmap/auto/MapScreen.kt | 104 +++++++++++------- .../net/vonforst/evmap/viewmodel/Common.kt | 81 ++++++++++++++ .../evmap/viewmodel/FilterViewModel.kt | 49 +-------- .../vonforst/evmap/viewmodel/MapViewModel.kt | 63 +++-------- 6 files changed, 263 insertions(+), 165 deletions(-) create mode 100644 app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt create mode 100644 app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt diff --git a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt index 139adae2..60fbe9a7 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt @@ -21,20 +21,16 @@ import net.vonforst.evmap.* import net.vonforst.evmap.api.availability.ChargeLocationStatus import net.vonforst.evmap.api.availability.getAvailability import net.vonforst.evmap.api.createApi -import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper import net.vonforst.evmap.api.nameForPlugType -import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.model.ChargeLocation -import net.vonforst.evmap.model.ReferenceData import net.vonforst.evmap.storage.AppDatabase -import net.vonforst.evmap.storage.GEReferenceDataRepository -import net.vonforst.evmap.storage.OCMReferenceDataRepository import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.ChargerIconGenerator import net.vonforst.evmap.ui.availabilityText import net.vonforst.evmap.ui.getMarkerTint import net.vonforst.evmap.viewmodel.Status +import net.vonforst.evmap.viewmodel.getReferenceData import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -50,9 +46,16 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : private val api by lazy { createApi(prefs.dataSource, ctx) } + private val referenceData = api.getReferenceData(lifecycleScope, carContext) private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64) + init { + referenceData.observe(this) { + loadCharger() + } + } + override fun onGetTemplate(): Template { if (charger == null) loadCharger() @@ -181,8 +184,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : } private fun loadCharger() { + val referenceData = referenceData.value ?: return lifecycleScope.launch { - val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id) + val response = api.getChargepointDetail(referenceData, chargerSparse.id) if (response.status == Status.SUCCESS) { charger = response.data!! @@ -206,29 +210,4 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : } } } - - private suspend fun getReferenceData(): ReferenceData { - val api = api - return when (api) { - is GoingElectricApiWrapper -> { - GEReferenceDataRepository( - api, - lifecycleScope, - db.geReferenceDataDao(), - prefs - ).getReferenceData().await() - } - is OpenChargeMapApiWrapper -> { - OCMReferenceDataRepository( - api, - lifecycleScope, - db.ocmReferenceDataDao(), - prefs - ).getReferenceData().await() - } - else -> { - throw RuntimeException("no reference data implemented") - } - } - } } \ No newline at end of file diff --git a/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt new file mode 100644 index 00000000..cae3a35b --- /dev/null +++ b/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt @@ -0,0 +1,90 @@ +package net.vonforst.evmap.auto + +import android.graphics.Bitmap +import androidx.car.app.CarContext +import androidx.car.app.Screen +import androidx.car.app.model.* +import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.LiveData +import net.vonforst.evmap.R +import net.vonforst.evmap.model.FILTERS_DISABLED +import net.vonforst.evmap.storage.AppDatabase +import net.vonforst.evmap.storage.FilterProfile +import net.vonforst.evmap.storage.PreferenceDataSource +import kotlin.math.roundToInt + +class FilterScreen(ctx: CarContext) : Screen(ctx) { + private val prefs = PreferenceDataSource(ctx) + private val db = AppDatabase.getInstance(ctx) + val filterProfiles: LiveData> by lazy { + db.filterProfileDao().getProfiles(prefs.dataSource) + } + private val maxRows = 6 + private val checkIcon = + CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check)).build() + private val emptyIcon: CarIcon + + init { + val size = (ctx.resources.displayMetrics.density * 24).roundToInt() + emptyIcon = CarIcon.Builder( + IconCompat.createWithBitmap( + Bitmap.createBitmap( + size, + size, + Bitmap.Config.ARGB_8888 + ) + ) + ).build() + } + + init { + filterProfiles.observe(this) { + invalidate() + } + } + + override fun onGetTemplate(): Template { + return ListTemplate.Builder().apply { + filterProfiles.value?.let { + setSingleList(buildFilterProfilesList(it.take(maxRows), prefs.filterStatus)) + } ?: setLoading(true) + setTitle(carContext.getString(R.string.menu_filter)) + setHeaderAction(Action.BACK) + }.build() + } + + private fun buildFilterProfilesList( + profiles: List, + filterStatus: Long + ): ItemList { + return ItemList.Builder().apply { + addItem(Row.Builder().apply { + setTitle(carContext.getString(R.string.no_filters)) + if (FILTERS_DISABLED == filterStatus) { + setImage(checkIcon) + } else { + setImage(emptyIcon) + } + setOnClickListener { + prefs.filterStatus = FILTERS_DISABLED + screenManager.pop() + } + }.build()) + profiles.forEach { + addItem(Row.Builder().apply { + setTitle(it.name) + if (it.id == filterStatus) { + setImage(checkIcon) + } else { + setImage(emptyIcon) + } + setOnClickListener { + prefs.filterStatus = it.id + screenManager.pop() + } + }.build()) + } + setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state)) + }.build() + } +} \ No newline at end of file diff --git a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt index e331e933..f91d0894 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt @@ -8,6 +8,8 @@ import androidx.car.app.CarToast import androidx.car.app.Screen import androidx.car.app.model.* import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import com.car2go.maps.model.LatLng import kotlinx.coroutines.* @@ -15,21 +17,23 @@ import net.vonforst.evmap.R import net.vonforst.evmap.api.availability.ChargeLocationStatus import net.vonforst.evmap.api.availability.getAvailability import net.vonforst.evmap.api.createApi -import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper -import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper -import net.vonforst.evmap.await +import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.model.ChargeLocation -import net.vonforst.evmap.model.ReferenceData +import net.vonforst.evmap.model.FILTERS_CUSTOM +import net.vonforst.evmap.model.FILTERS_DISABLED import net.vonforst.evmap.storage.AppDatabase -import net.vonforst.evmap.storage.GEReferenceDataRepository -import net.vonforst.evmap.storage.OCMReferenceDataRepository import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.availabilityText import net.vonforst.evmap.ui.getMarkerTint import net.vonforst.evmap.utils.distanceBetween +import net.vonforst.evmap.viewmodel.filtersWithValue +import net.vonforst.evmap.viewmodel.getFilterValues +import net.vonforst.evmap.viewmodel.getFilters +import net.vonforst.evmap.viewmodel.getReferenceData import java.io.IOException import java.time.Duration import java.time.ZonedDateTime +import kotlin.collections.set import kotlin.math.roundToInt /** @@ -46,6 +50,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole private var lastUpdateLocation: Location? = null private var chargers: List? = null private var prefs = PreferenceDataSource(ctx) + private val db = AppDatabase.getInstance(carContext) private val api by lazy { createApi(prefs.dataSource, ctx) } @@ -56,6 +61,20 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole HashMap() private val maxRows = 6 + private val referenceData = api.getReferenceData(lifecycleScope, carContext) + private val filterStatus = MutableLiveData().apply { + value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM } ?: FILTERS_DISABLED + } + private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource) + private val filters = api.getFilters(referenceData, carContext.stringProvider()) + private val filtersWithValue = filtersWithValue(filters, filterValues) + + init { + filtersWithValue.observe(this) { + loadChargers() + } + } + override fun onGetTemplate(): Template { session.mapScreen = this return PlaceListMapTemplate.Builder().apply { @@ -91,6 +110,36 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole } ?: setLoading(true) setCurrentLocationEnabled(true) setHeaderAction(Action.BACK) + if (!favorites) { + val filtersCount = filtersWithValue.value?.count { + !it.value.hasSameValueAs(it.filter.defaultValue()) + } + + setActionStrip( + ActionStrip.Builder() + .addAction( + Action.Builder() + .setIcon( + CarIcon.Builder( + IconCompat.createWithResource( + carContext, + R.drawable.ic_filter + ) + ) + .setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT) + .build() + ) + .setOnClickListener { + screenManager.pushForResult(FilterScreen(carContext)) { + chargers = null + numUpdates = 0 + filterStatus.value = prefs.filterStatus + } + session.mapScreen = null + } + .build()) + .build()) + } build() }.build() } @@ -178,13 +227,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole ) { lastUpdateLocation = location // update displayed chargers - loadChargers(location) + loadChargers() } } - private val db = AppDatabase.getInstance(carContext) + private fun loadChargers() { + val location = location ?: return + val referenceData = referenceData.value ?: return + val filters = filtersWithValue.value ?: return - private fun loadChargers(location: Location) { numUpdates++ println(numUpdates) if (numUpdates > maxNumUpdates) { @@ -204,22 +255,22 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole } } else { val response = api.getChargepointsRadius( - getReferenceData(), + referenceData, LatLng.fromLocation(location), searchRadius, zoom = 16f, - null + filters ) chargers = response.data?.filterIsInstance(ChargeLocation::class.java) chargers?.let { if (it.size < 6) { // try again with larger radius val response = api.getChargepointsRadius( - getReferenceData(), + referenceData, LatLng.fromLocation(location), - searchRadius * 5, + searchRadius * 10, zoom = 16f, - emptyList() + filters ) chargers = response.data?.filterIsInstance(ChargeLocation::class.java) @@ -259,29 +310,4 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole } } } - - private suspend fun getReferenceData(): ReferenceData { - val api = api - return when (api) { - is GoingElectricApiWrapper -> { - GEReferenceDataRepository( - api, - lifecycleScope, - db.geReferenceDataDao(), - prefs - ).getReferenceData().await() - } - is OpenChargeMapApiWrapper -> { - OCMReferenceDataRepository( - api, - lifecycleScope, - db.ocmReferenceDataDao(), - prefs - ).getReferenceData().await() - } - else -> { - throw RuntimeException("no reference data implemented") - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt new file mode 100644 index 00000000..45180845 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt @@ -0,0 +1,81 @@ +package net.vonforst.evmap.viewmodel + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import kotlinx.coroutines.CoroutineScope +import net.vonforst.evmap.api.ChargepointApi +import net.vonforst.evmap.api.StringProvider +import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper +import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper +import net.vonforst.evmap.model.* +import net.vonforst.evmap.storage.* +import kotlin.reflect.full.cast + +fun ChargepointApi.getReferenceData( + scope: CoroutineScope, + ctx: Context +): LiveData { + val db = AppDatabase.getInstance(ctx) + val prefs = PreferenceDataSource(ctx) + return when (this) { + is GoingElectricApiWrapper -> { + GEReferenceDataRepository( + this, + scope, + db.geReferenceDataDao(), + prefs + ).getReferenceData() + } + is OpenChargeMapApiWrapper -> { + OCMReferenceDataRepository( + this, + scope, + db.ocmReferenceDataDao(), + prefs + ).getReferenceData() + } + else -> { + throw RuntimeException("no reference data implemented") + } + } +} + +fun filtersWithValue( + filters: LiveData>>, + filterValues: LiveData> +): MediatorLiveData = + MediatorLiveData().apply { + listOf(filters, filterValues).forEach { + addSource(it) { + val f = filters.value ?: return@addSource + val values = filterValues.value ?: return@addSource + value = f.map { filter -> + val value = + values.find { it.key == filter.key } ?: filter.defaultValue() + FilterWithValue(filter, filter.valueClass.cast(value)) + } + } + } + } + +fun ChargepointApi.getFilters( + referenceData: LiveData, + stringProvider: StringProvider +) = MediatorLiveData>>().apply { + addSource(referenceData) { data -> + value = getFilters(data, stringProvider) + } +} + +fun FilterValueDao.getFilterValues(filterStatus: LiveData, dataSource: String) = + MediatorLiveData>().apply { + var source: LiveData>? = null + addSource(filterStatus) { status -> + source?.let { removeSource(it) } + source = getFilterValues(status, dataSource) + addSource(source!!) { result -> + value = result + } + } + } \ 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 2f70c740..3976eddf 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt @@ -5,60 +5,19 @@ import androidx.lifecycle.* import kotlinx.coroutines.launch import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.createApi -import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper -import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.model.* -import net.vonforst.evmap.storage.* -import kotlin.reflect.full.cast +import net.vonforst.evmap.storage.AppDatabase +import net.vonforst.evmap.storage.FilterProfile +import net.vonforst.evmap.storage.PreferenceDataSource -internal fun filtersWithValue( - filters: LiveData>>, - filterValues: LiveData> -): MediatorLiveData = - MediatorLiveData().apply { - listOf(filters, filterValues).forEach { - addSource(it) { - val f = filters.value ?: return@addSource - val values = filterValues.value ?: return@addSource - value = f.map { filter -> - val value = - values.find { it.key == filter.key } ?: filter.defaultValue() - FilterWithValue(filter, filter.valueClass.cast(value)) - } - } - } - } class FilterViewModel(application: Application) : AndroidViewModel(application) { private var db = AppDatabase.getInstance(application) private var prefs = PreferenceDataSource(application) private var api: ChargepointApi = createApi(prefs.dataSource, application) - private val referenceData: LiveData by lazy { - val api = api - when (api) { - is GoingElectricApiWrapper -> { - GEReferenceDataRepository( - api, - viewModelScope, - db.geReferenceDataDao(), - prefs - ).getReferenceData() - } - is OpenChargeMapApiWrapper -> { - OCMReferenceDataRepository( - api, - viewModelScope, - db.ocmReferenceDataDao(), - prefs - ).getReferenceData() - } - else -> { - throw RuntimeException("no reference data implemented") - } - } - } + private val referenceData = api.getReferenceData(viewModelScope, application) private val filters = MediatorLiveData>>().apply { addSource(referenceData) { data -> value = api.getFilters(data, application.stringProvider()) 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 8ef38b89..1f52d98f 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -19,7 +19,9 @@ import net.vonforst.evmap.api.openchargemap.OCMReferenceData import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.api.stringProvider import net.vonforst.evmap.model.* -import net.vonforst.evmap.storage.* +import net.vonforst.evmap.storage.AppDatabase +import net.vonforst.evmap.storage.FilterProfile +import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.utils.distanceBetween import java.io.IOException @@ -54,48 +56,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { val mapPosition: MutableLiveData by lazy { MutableLiveData() } - private val filterValues: LiveData> by lazy { - MediatorLiveData>().apply { - var source: LiveData>? = null - addSource(filterStatus) { status -> - source?.let { removeSource(it) } - source = db.filterValueDao().getFilterValues(status, prefs.dataSource) - addSource(source!!) { result -> - value = result - } + val filterStatus: MutableLiveData by lazy { + MutableLiveData().apply { + value = prefs.filterStatus + observeForever { + prefs.filterStatus = it + if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it } } } - private val referenceData: LiveData by lazy { - val api = api - when (api) { - is GoingElectricApiWrapper -> { - GEReferenceDataRepository( - api, - viewModelScope, - db.geReferenceDataDao(), - prefs - ).getReferenceData() - } - is OpenChargeMapApiWrapper -> { - OCMReferenceDataRepository( - api, - viewModelScope, - db.ocmReferenceDataDao(), - prefs - ).getReferenceData() - } - else -> { - throw RuntimeException("no reference data implemented") - } - } - } - private val filters = MediatorLiveData>>().apply { - addSource(referenceData) { data -> - val api = api - value = api.getFilters(data, application.stringProvider()) - } - } + private val filterValues: LiveData> = + db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource) + private val referenceData = api.getReferenceData(viewModelScope, application) + private val filters = api.getFilters(referenceData, application.stringProvider()) private val filtersWithValue: LiveData by lazy { filtersWithValue(filters, filterValues) @@ -271,16 +244,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { } } - val filterStatus: MutableLiveData by lazy { - MutableLiveData().apply { - value = prefs.filterStatus - observeForever { - prefs.filterStatus = it - if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it - } - } - } - fun reloadPrefs() { filterStatus.value = prefs.filterStatus }