Android Auto: start implementing creation of filter profiles

#172
This commit is contained in:
johan12345
2022-06-24 19:49:39 +02:00
parent fa2b7bf180
commit abec208768
5 changed files with 250 additions and 26 deletions

View File

@@ -1,25 +1,31 @@
package net.vonforst.evmap.auto
import android.app.Application
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.FilterViewModel
@androidx.car.app.annotations.ExperimentalCarApi
class FilterScreen(ctx: CarContext) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(ctx)
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
private val maxRows = 6
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private val checkedIcon =
CarIcon.Builder(
IconCompat.createWithResource(
@@ -47,11 +53,10 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
override fun onGetTemplate(): Template {
val filterStatus =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES } ?: FILTERS_DISABLED
return ListTemplate.Builder().apply {
filterProfiles.value?.let {
setSingleList(buildFilterProfilesList(it.take(maxRows), filterStatus))
setSingleList(buildFilterProfilesList(it, filterStatus))
} ?: setLoading(true)
setTitle(carContext.getString(R.string.menu_filter))
setHeaderAction(Action.BACK)
@@ -62,6 +67,8 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
profiles: List<FilterProfile>,
filterStatus: Long
): ItemList {
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
val profilesToShow = profiles.take(maxRows - extraRows)
return ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.no_filters))
@@ -71,11 +78,10 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
setImage(uncheckedIcon)
}
setOnClickListener {
prefs.filterStatus = FILTERS_DISABLED
screenManager.pop()
onItemClick(FILTERS_DISABLED)
}
}.build())
profiles.forEach {
profilesToShow.forEach {
addItem(Row.Builder().apply {
val name =
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
@@ -86,12 +92,188 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
setImage(uncheckedIcon)
}
setOnClickListener {
prefs.filterStatus = it.id
screenManager.pop()
onItemClick(it.id)
}
}.build())
}
setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state))
if (FILTERS_CUSTOM == filterStatus) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.filter_custom))
setImage(checkedIcon)
setOnClickListener {
onItemClick(FILTERS_CUSTOM)
}
}.build())
}
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.menu_edit_filters))
setOnClickListener(ParkedOnlyOnClickListener.create {
lifecycleScope.launch {
db.filterValueDao().copyFiltersToCustom(filterStatus, prefs.dataSource)
screenManager.push(EditFiltersScreen(carContext))
}
})
}.build())
}.build()
}
private fun onItemClick(id: Long) {
prefs.filterStatus = id
screenManager.pop()
}
}
@androidx.car.app.annotations.ExperimentalCarApi
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
private val vm = FilterViewModel(carContext.applicationContext as Application)
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
init {
vm.filtersWithValue.observe(this) {
vm.filterProfile.observe(this) {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
val currentProfileName = vm.filterProfile.value?.name
return ListTemplate.Builder().apply {
vm.filtersWithValue.value?.let { filtersWithValue ->
setSingleList(buildFiltersList(filtersWithValue.take(maxRows)))
} ?: setLoading(true)
setTitle(currentProfileName?.let {
carContext.getString(
R.string.edit_filter_profile,
it
)
} ?: carContext.getString(R.string.menu_filter))
setHeaderAction(Action.BACK)
setActionStrip(ActionStrip.Builder().apply {
addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_check
)
).build()
)
.setOnClickListener {
lifecycleScope.launch {
vm.saveFilterValues()
screenManager.popTo(MapScreen.MARKER)
}
}
.build()
)
addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_save
)
).build()
)
.setOnClickListener {
val textPromptScreen = TextPromptScreen(
carContext,
R.string.save_as_profile,
R.string.save_profile_enter_name,
currentProfileName
)
screenManager.pushForResult(textPromptScreen) { name ->
if (name == null) return@pushForResult
lifecycleScope.launch {
vm.saveAsProfile(name as String)
screenManager.popTo(MapScreen.MARKER)
}
}
}
.build()
)
}.build())
}.build()
}
private fun buildFiltersList(filters: List<FilterWithValue<out FilterValue>>): ItemList {
return ItemList.Builder().apply {
filters.forEach {
val filter = it.filter
val value = it.value
addItem(Row.Builder().apply {
setTitle(filter.name)
when (filter) {
is BooleanFilter -> {
setToggle(Toggle.Builder {
(value as BooleanFilterValue).value = it
}.setChecked((value as BooleanFilterValue).value).build())
}
is MultipleChoiceFilter -> {
setOnClickListener {
screenManager.push(
MultipleChoiceFilterScreen(
carContext,
filter,
value as MultipleChoiceFilterValue
)
)
}
}
is SliderFilter -> {
// TODO: toggle through possible options on click?
}
}
}.build())
}
}.build()
}
}
class MultipleChoiceFilterScreen(
ctx: CarContext,
val filter: MultipleChoiceFilter,
val value: MultipleChoiceFilterValue
) : MultiSelectSearchScreen<Pair<String, String>>(ctx) {
override val isMultiSelect = true
override val shouldShowSelectAll = true
override fun isSelected(it: Pair<String, String>): Boolean =
value.all || value.values.contains(it.first)
override fun toggleSelected(item: Pair<String, String>) {
if (isSelected(item)) {
val values = if (value.all) filter.choices.keys else value.values
value.values = values.minus(item.first).toMutableSet()
value.all = false
} else {
value.values.add(item.first)
if (value.values == filter.choices.keys) {
value.all = true
}
}
}
override fun selectAll() {
value.all = true
}
override fun selectNone() {
value.all = false
value.values = mutableSetOf()
}
override fun getLabel(it: Pair<String, String>): String = it.second
override suspend fun loadData(): List<Pair<String, String>> {
return filter.choices.entries.map { it.toPair() }
}
}

View File

@@ -23,7 +23,6 @@ import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.storage.AppDatabase
@@ -48,6 +47,10 @@ import kotlin.math.roundToInt
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
ItemList.OnItemVisibilityChangedListener {
companion object {
val MARKER = "map"
}
private var updateCoroutine: Job? = null
private var availabilityUpdateCoroutine: Job? = null
@@ -73,8 +76,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
value = prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES } ?: FILTERS_DISABLED
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters =
@@ -99,6 +101,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
filtersWithValue.observe(this) {
loadChargers()
}
marker = MARKER
}
override fun onGetTemplate(): Template {
@@ -160,7 +163,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
screenManager.pushForResult(FilterScreen(carContext)) {
chargers = null
filterStatus.value =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
prefs.filterStatus.takeUnless { it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
}
session.mapScreen = null

View File

@@ -0,0 +1,35 @@
package net.vonforst.evmap.auto
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.InputCallback
import androidx.car.app.model.Template
import androidx.car.app.model.signin.InputSignInMethod
import androidx.car.app.model.signin.SignInTemplate
class TextPromptScreen(
ctx: CarContext,
@StringRes val title: Int,
@StringRes val prompt: Int,
val initialValue: String? = null
) : Screen(ctx),
InputCallback {
override fun onGetTemplate(): Template {
val signInMethod = InputSignInMethod.Builder(this).apply {
initialValue?.let { setDefaultValue(it) }
setShowKeyboardByDefault(true)
}.build()
return SignInTemplate.Builder(signInMethod).apply {
setHeaderAction(Action.BACK)
setInstructions(carContext.getString(prompt))
setTitle(carContext.getString(title))
}.build()
}
override fun onInputSubmitted(text: String) {
setResult(text)
screenManager.pop()
}
}

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.*
import net.vonforst.evmap.await
import net.vonforst.evmap.model.*
@Dao
@@ -92,4 +93,15 @@ abstract class FilterValueDao {
deleteSliderFilterValuesForProfile(profile, dataSource)
}
@Transaction
open suspend fun copyFiltersToCustom(filterStatus: Long, dataSource: String) {
if (filterStatus == FILTERS_CUSTOM) return
deleteFilterValuesForProfile(FILTERS_CUSTOM, dataSource)
val values = getFilterValues(filterStatus, dataSource).await().onEach {
it.profile = FILTERS_CUSTOM
}
insert(*values.toTypedArray())
}
}

View File

@@ -270,15 +270,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
suspend fun copyFiltersToCustom() {
if (filterStatus.value == FILTERS_CUSTOM) return
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
filterValues.value?.map {
it.profile = FILTERS_CUSTOM
it
}?.let {
db.filterValueDao().insert(*it.toTypedArray())
}
filterStatus.value?.let { db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource) }
}
fun setMapType(type: AnyMap.Type) {