create separate database table for favorites

to make ChargeLocation table usable for caching and offline storage (#88, #97) and to allow for multiple favorites lists later (#127)
This commit is contained in:
johan12345
2022-01-23 19:28:24 +01:00
parent ac3d0b0eb0
commit e505fea043
9 changed files with 144 additions and 44 deletions

View File

@@ -268,12 +268,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
try {
// load chargers
if (favorites) {
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
chargers =
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
} else {
val response = api.getChargepointsRadius(
referenceData,

View File

@@ -14,7 +14,7 @@ class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> U
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun getItemId(position: Int): Long = getItem(position).charger.id
override fun getItemId(position: Int): Long = getItem(position).fav.favorite.favoriteId
@SuppressLint("ClickableViewAccessibility")
override fun bind(

View File

@@ -28,7 +28,8 @@ import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FavoritesAdapter
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -36,7 +37,7 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
private lateinit var binding: FragmentFavoritesBinding
private var locationClient: LostApiClient? = null
private var toDelete: ChargeLocation? = null
private var toDelete: Favorite? = null
private var deleteSnackbar: Snackbar? = null
private lateinit var adapter: FavoritesAdapter
@@ -84,7 +85,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
)
adapter = FavoritesAdapter(onDelete = {
delete(it.charger)
delete(it.fav)
}).apply {
onClickListener = {
findNavController().navigate(
@@ -132,18 +133,20 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
}
fun delete(fav: ChargeLocation) {
val position = vm.listData.value?.indexOfFirst { it.charger == fav } ?: return
fun delete(fav: FavoriteWithDetail) {
val position =
vm.listData.value?.indexOfFirst { it.fav.favorite.favoriteId == fav.favorite.favoriteId }
?: return
// if there is already a profile to delete, delete it now
actuallyDelete()
deleteSnackbar?.dismiss()
toDelete = fav
toDelete = fav.favorite
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fav.name),
getString(R.string.deleted_filterprofile, fav.charger.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null
@@ -182,7 +185,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val fav = vm.favorites.value?.find { it.id == viewHolder.itemId }
val fav = vm.favorites.value?.find { it.favorite.favoriteId == viewHolder.itemId }
fav?.let { delete(it) }
}

View File

@@ -499,9 +499,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
val isFav = favs.find { it.id == charger.id } != null
if (isFav) {
vm.deleteFavorite(charger)
val fav = favs.find { it.charger.id == charger.id }
if (fav != null) {
vm.deleteFavorite(fav.favorite)
} else {
vm.insertFavorite(charger)
}
@@ -511,7 +511,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = !isFav
fav = fav == null
)
)
}
@@ -642,7 +642,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -657,7 +657,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
animator.animateMarkerBounce(marker)
@@ -671,7 +671,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -681,7 +681,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
if (favs.find { it.charger.id == charger.id } != null) {
favToggle.setIcon(R.drawable.ic_fav)
} else {
favToggle.setIcon(R.drawable.ic_fav_no)
@@ -1023,7 +1023,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -1041,7 +1041,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val fav =
charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav)
} else {
animator.deleteMarker(marker)
@@ -1057,7 +1058,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))

View File

@@ -0,0 +1,28 @@
package net.vonforst.evmap.model
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = ChargeLocation::class,
parentColumns = arrayOf("id", "dataSource"),
childColumns = arrayOf("chargerId", "chargerDataSource"),
onDelete = ForeignKey.RESTRICT,
)
]
)
data class Favorite(
@PrimaryKey(autoGenerate = true)
val favoriteId: Long = 0,
val chargerId: Long,
val chargerDataSource: String
)
data class FavoriteWithDetail(
@Embedded() val favorite: Favorite,
@Embedded val charger: ChargeLocation
)

View File

@@ -20,6 +20,7 @@ import net.vonforst.evmap.model.*
@Database(
entities = [
ChargeLocation::class,
Favorite::class,
BooleanFilterValue::class,
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
@@ -31,11 +32,12 @@ import net.vonforst.evmap.model.*
OCMConnectionType::class,
OCMCountry::class,
OCMOperator::class
], version = 14
], version = 15
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun favoritesDao(): FavoritesDao
abstract fun filterValueDao(): FilterValueDao
abstract fun filterProfileDao(): FilterProfileDao
abstract fun recentAutocompletePlaceDao(): RecentAutocompletePlaceDao
@@ -53,7 +55,7 @@ abstract class AppDatabase : RoomDatabase() {
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13, MIGRATION_14
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -312,5 +314,30 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_15 = object : Migration(14, 15) {
@SuppressLint("Range")
override fun migrate(db: SupportSQLiteDatabase) {
try {
db.beginTransaction()
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )");
val cursor = db.query("SELECT * FROM `ChargeLocation`")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndex("id"))
val dataSource = cursor.getString(cursor.getColumnIndex("dataSource"))
val values = ContentValues().apply {
put("chargerId", id)
put("chargerDataSource", dataSource)
}
db.insert("favorite", SQLiteDatabase.CONFLICT_ROLLBACK, values)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
@Dao
interface FavoritesDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg favorites: Favorite)
@Delete
suspend fun delete(vararg favorites: Favorite)
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
fun getAllFavorites(): LiveData<List<FavoriteWithDetail>>
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
suspend fun getAllFavoritesAsync(): List<FavoriteWithDetail>
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getFavoritesInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<FavoriteWithDetail>
}

View File

@@ -11,6 +11,8 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.utils.distanceBetween
@@ -18,8 +20,8 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
db.favoritesDao().getAllFavorites()
}
val location: MutableLiveData<LatLng> by lazy {
@@ -28,8 +30,9 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
val availability: MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>> by lazy {
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
addSource(favorites) { chargers ->
if (chargers != null) {
addSource(favorites) { favorites ->
if (favorites != null) {
val chargers = favorites.map { it.charger }
viewModelScope.launch {
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
chargers.forEach { charger ->
@@ -54,9 +57,10 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
val listData: MediatorLiveData<List<FavoritesListItem>> by lazy {
MediatorLiveData<List<FavoritesListItem>>().apply {
val callback = { _: Any ->
listData.value = favorites.value?.map { charger ->
listData.value = favorites.value?.map { favorite ->
val charger = favorite.charger
FavoritesListItem(
charger,
favorite,
totalAvailable(charger.id),
charger.chargepoints.sumBy { it.count },
location.value.let { loc ->
@@ -78,11 +82,14 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
}
data class FavoritesListItem(
val charger: ChargeLocation,
val fav: FavoriteWithDetail,
val available: Resource<List<ChargepointStatus>>,
val total: Int,
val distance: Double?
) : Equatable
) : Equatable {
val charger
get() = fav.charger
}
private fun totalAvailable(id: Long): Resource<List<ChargepointStatus>> {
val availability = availability.value?.get(id) ?: return Resource.error(null, null)
@@ -97,12 +104,14 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
db.favoritesDao()
.insert(Favorite(chargerId = charger.id, chargerDataSource = charger.dataSource))
}
}
fun deleteFavorite(charger: ChargeLocation) {
fun deleteFavorite(fav: Favorite) {
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
db.favoritesDao().delete(fav)
}
}
}

View File

@@ -222,8 +222,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
db.favoritesDao().getAllFavorites()
}
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
@@ -279,12 +279,14 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
db.favoritesDao()
.insert(Favorite(chargerId = charger.id, chargerDataSource = charger.dataSource))
}
}
fun deleteFavorite(charger: ChargeLocation) {
fun deleteFavorite(favorite: Favorite) {
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
db.favoritesDao().delete(favorite)
}
}
@@ -310,12 +312,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
val b = mapPosition.bounds
var chargers = db.chargeLocationsDao().getChargeLocationsInBoundsAsync(
var chargers = db.favoritesDao().getFavoritesInBoundsAsync(
b.southwest.latitude,
b.northeast.latitude,
b.southwest.longitude,
b.northeast.longitude
) as List<ChargepointListItem>
).map { it.charger } as List<ChargepointListItem>
val clusterDistance = getClusterDistance(mapPosition.zoom)
clusterDistance?.let {