Author: Adam Evyčędo <git@apiote.xyz>
add diffUtils to favourites
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt index 689434ea950b314a6b7faa16ffd2c6c6cdbb174c..b3cb8722547c815be4076bc24615a101aba0a8f6 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt @@ -11,22 +11,80 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.departures.DeparturesActivity import xyz.apiote.bimba.czwek.repo.Departure import xyz.apiote.bimba.czwek.repo.Favourite +import java.time.ZoneId +import java.time.ZonedDateTime import java.util.Collections import java.util.Optional -// TODO diffUtil class BimbaFavouritesAdapter( private var favourites: List<Favourite>, - private var departures: List<Optional<Departure>?>, + private var departures: Map<String, Optional<Departure>>, private val inflater: LayoutInflater, private val context: Context ) : RecyclerView.Adapter<FavouriteViewHolder>() { + var lastUpdate: ZonedDateTime = + ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) + private set + + inner class DiffUtilCallback( + private val oldFavourites: List<Favourite>, + private val oldDepartures: Map<String, Optional<Departure>?>, + private val newFavourites: List<Favourite>, + private val newDepartures: Map<String, Optional<Departure>?> + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldFavourites.size + + override fun getNewListSize() = newFavourites.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldFavourites[oldItemPosition].feedID + oldFavourites[oldItemPosition].stopCode == newFavourites[newItemPosition].feedID + newFavourites[newItemPosition].stopCode + + @Suppress("KotlinConstantConditions", "UNNECESSARY_NOT_NULL_ASSERTION") + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldFav = oldFavourites[oldItemPosition] + val newFav = newFavourites[newItemPosition] + val oldDeparture = oldDepartures[oldFav.feedID + oldFav.stopCode] + val newDeparture = newDepartures[oldFav.feedID + oldFav.stopCode] + + if ((oldDeparture == null && newDeparture != null) || (oldDeparture != null && newDeparture == null)) { + return false + } + + val favouritesSame = oldFav.feedName == newFav.feedName && + oldFav.stopName == newFav.stopName && + oldFav.sequence == newFav.sequence && + oldFav.lines == newFav.lines + + if (!favouritesSame) { + return false + } + + if ((oldDeparture == null && newDeparture == null) || (oldDeparture!!.isEmpty && newDeparture!!.isEmpty)) { + return true + } + + if ((oldDeparture!!.isEmpty && !newDeparture!!.isEmpty) || (!oldDeparture!!.isEmpty && newDeparture!!.isEmpty)) { + return false + } + + return oldDeparture!!.get().ID == newDeparture!!.get().ID && + oldDeparture!!.get().vehicle.Line == newDeparture!!.get().vehicle.Line && + oldDeparture!!.get().vehicle.Headsign == newDeparture!!.get().vehicle.Headsign && + oldDeparture!!.get().statusText( + context, + false, + lastUpdate + ) == newDeparture!!.get().statusText(context, false) + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavouriteViewHolder { val rowView = inflater.inflate(R.layout.favourite, parent, false) return FavouriteViewHolder(rowView) @@ -35,23 +93,33 @@ override fun getItemCount() = favourites.size override fun onBindViewHolder(holder: FavouriteViewHolder, position: Int) { - FavouriteViewHolder.bind(favourites[position], holder, context, departures.getOrNull(position)) + FavouriteViewHolder.bind( + favourites[position], holder, context, + departures[favourites[position].feedID + favourites[position].stopCode] + ) } fun updateFavourites(favourites: List<Favourite>) { + val diff = DiffUtil.calculateDiff( + DiffUtilCallback( + this.favourites, + this.departures, + favourites, + this.departures + ) + ) this.favourites = favourites - departures = favourites.map { Optional.empty() } - notifyDataSetChanged() + diff.dispatchUpdatesTo(this) } - fun updateDepartures(departures: List<Departure?>) { - this.departures = departures.map { Optional.ofNullable(it) } + fun updateDepartures(departures: Map<String, Optional<Departure>>) { + this.departures = departures notifyDataSetChanged() + lastUpdate = ZonedDateTime.now() } fun swap(from: Int, to: Int): List<Favourite> { Collections.swap(favourites, from, to) - Collections.swap(departures, from, to) favourites = favourites.mapIndexed { i, it -> it.copy(sequence = i) } @@ -101,7 +169,7 @@ if (departure == null) { holder.feedName.text = favourite.feedName holder.stopHeadline.text = favourite.stopName holder.lineIcon.setImageDrawable(null) - holder.lineName.text = context.getString(R.string.no_next_departures) + holder.lineName.text = context.getString(R.string.loading) holder.departureTime.text = "" holder.departureTimeFull.text = "" holder.headsign.text = "" @@ -109,7 +177,7 @@ } else if (departure.isEmpty) { holder.feedName.text = favourite.feedName holder.stopHeadline.text = favourite.stopName holder.lineIcon.setImageDrawable(null) - holder.lineName.text = context.getString(R.string.loading) + holder.lineName.text = context.getString(R.string.no_next_departures) holder.departureTime.text = "" holder.departureTimeFull.text = "" holder.headsign.text = "" diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt index f4dee33d49e23823cca32aab707a5956f962a5cd..d3d3c1afc89244763f309441eb5c24275862d30d 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt @@ -152,7 +152,7 @@ windowInsets } binding.favourites.layoutManager = LinearLayoutManager(context) - favouritesAdapter = BimbaFavouritesAdapter(listOf(), listOf(), layoutInflater, requireContext()) + favouritesAdapter = BimbaFavouritesAdapter(listOf(), mapOf(), layoutInflater, requireContext()) binding.favourites.adapter = favouritesAdapter viewModel.getFavourites(requireContext()) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt index 1dd82ec49e908b7aa49a50923318ae7227620818..ba6da66f9308e7dc91bd81aca46cff37c3dffee1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt @@ -18,8 +18,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import xyz.apiote.bimba.czwek.api.Error -import xyz.apiote.bimba.czwek.api.mapHttpError import xyz.apiote.bimba.czwek.repo.Departure import xyz.apiote.bimba.czwek.repo.Favourite import xyz.apiote.bimba.czwek.repo.FeedInfo @@ -29,6 +27,7 @@ import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings import java.sql.SQLException +import java.util.Optional class HomeViewModel : ViewModel() { private val mutableQueryables = MutableLiveData<List<Queryable>>() @@ -37,8 +36,8 @@ var feeds: Map? = null var feedsSettings: FeedsSettings? = null private val mutableFavourites = MutableLiveData<List<Favourite>>() val favourites: LiveData<List<Favourite>> = mutableFavourites - private val mutableDepartures = MutableLiveData<List<Departure?>>() - val departures: LiveData<List<Departure?>> = mutableDepartures + private val mutableDepartures = MutableLiveData<Map<String, Optional<Departure>>>() + val departures: LiveData<Map<String, Optional<Departure>>> = mutableDepartures fun getQueryables(query: String, context: Context) { viewModelScope.launch { @@ -59,6 +58,7 @@ getFeeds(context) val repository = OfflineRepository(context) mutableFavourites.value = repository.getFavourites(feedsSettings?.activeFeeds() ?: emptySet()) + repository.close() } catch (e: SQLException) { Log.w("FavouritesForFavourite", "$e") } @@ -88,28 +88,30 @@ null, context, 12 // XXX heuristics ) - stopDepartures?.let { sDs -> - if (sDs.departures.isEmpty()) { - val (string, image) = mapHttpError(44) - throw TrafficResponseException(44, "", Error(44, string, image)) - } - sDs.departures.find { departure -> - favourite.lines.isEmpty() or favourite.lines.contains( - departure.vehicle.Line.name - ) - } - } + stopDepartures?.let { sDs -> + if (sDs.departures.isEmpty()) { + Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + } else { + Pair(favourite.feedID+favourite.stopCode, Optional.ofNullable(sDs.departures.find { departure -> + favourite.lines.isEmpty() or favourite.lines.contains( + departure.vehicle.Line.name + ) + })) + } + } ?: Pair(favourite.feedID+favourite.stopCode, Optional.empty()) } catch (e: TrafficResponseException) { Log.w("DeparturesForFavourite", "$e") - null + Pair(favourite.feedID+favourite.stopCode, Optional.empty()) } } - }.awaitAll() + }.awaitAll().associate { it } } } private suspend fun getFeeds(context: Context) { - feeds = OfflineRepository(context).getFeeds(context) + val repository = OfflineRepository(context) + feeds = repository.getFeeds(context) + repository.close() feedsSettings = FeedsSettings.load(context) } @@ -119,6 +121,7 @@ try { val repository = OfflineRepository(context) repository.saveFavourites(newFavourites.toSet()) mutableFavourites.value = newFavourites + repository.close() } catch (e: SQLException) { Log.w("FavouritesForFavourite", "$e") } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt index 456340e38d76dcd0a0e63ea94a8bc7d8165c4670..783606925186535e8a5ba30761e3b1ae932b6b2d 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt @@ -187,9 +187,12 @@ binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).setEnabled(false) // TODO async esp. with Online if (runBlocking { - OfflineRepository(this@DeparturesActivity).getFavourite( + val repository = OfflineRepository(this@DeparturesActivity) + val f = repository.getFavourite( getCode() ?: "" ) + repository.close() + f } != null) { binding.departuresAppBar.menu.findItem(R.id.favourite).setIcon(R.drawable.favourite_full) } @@ -439,7 +442,11 @@ binding.errorImage.visibility = View.GONE binding.errorText.visibility = View.GONE } - private fun updateItems(departures: List<DepartureItem>, stop: Stop?, leaveAlert: Boolean = false) { + private fun updateItems( + departures: List<DepartureItem>, + stop: Stop?, + leaveAlert: Boolean = false + ) { setupSnackbar() binding.departuresRecycler.scrollToPosition(0) binding.departuresProgress.visibility = View.GONE @@ -504,6 +511,7 @@ getName(), linesFilter.toList() )).copy(lines = linesFilter.toList()) repo.saveFavourite(favourite) + repo.close() } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt index 21950564af8d8c98ac0010be649143bcb7e3d6e6..9d3b81505bb8907710dcf14bb6d543a1c5c1e3bf 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt @@ -37,9 +37,11 @@ var openBottomSheet: DepartureBottomSheet? = null private lateinit var code: String val mutableLinesFilter = MutableLiveData<Map<String, Boolean>>() val linesFilter: LiveData<Map<String, Boolean>> = mutableLinesFilter + // TODO observe in activity, maybe refreshing and not getting departures is enough var startTime: LocalTime = LocalTime.MIN var endTime: LocalTime = LocalTime.MAX + // TODO observe in activity, refreshing is not enough var date: LocalDate? = null @@ -78,13 +80,15 @@ } private suspend fun getFeed(context: Context): FeedInfo { val intent = (context as Activity).intent - var feeds = OfflineRepository(context).getFeeds(context) + val repository = OfflineRepository(context) + var feeds = repository.getFeeds(context) if (feeds.isNullOrEmpty()) { feeds = OnlineRepository().getFeeds(context) if (feeds != null) { - OfflineRepository(context).saveFeedCache(context, feeds) + repository.saveFeedCache(context, feeds) } } + repository.close() return when (intent.action) { Intent.ACTION_VIEW -> { val feed = feeds?.values?.find { it.qrHost == intent.data?.host } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt index f208f836c8d7c8cc4f48480df7a8f24e77846c14..165e9c932064611d7cf5be4f334a473356886eb9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt @@ -200,6 +200,10 @@ context: Context ): List<Queryable>? { TODO("Not yet implemented") } + + fun close() { + db.close() + } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index 387f9b58407aecb312d1875780b0d24104c1d7b0..f8729039653a32172c5b25a51668ff9ddfb15a6e 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt @@ -153,7 +153,9 @@ } private suspend fun getFeeds() { if (adapter.feeds.isNullOrEmpty()) { - adapter.feeds = OfflineRepository(this).getFeeds(this) + val repository = OfflineRepository(this) + adapter.feeds = repository.getFeeds(this) + repository.close() } if (adapter.feedsSettings == null) { adapter.feedsSettings = FeedsSettings.load(this) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt index 95ea23d25bf9938f5d220360ba71245508a06c1e..c178b2c8ba05f338651ef3d93bb0580ac2f6a3a1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt @@ -51,8 +51,10 @@ var error: Error? = null MainScope().launch { withContext(coroutineContext) { launch { + val repository = OfflineRepository(context) offlineFeeds = - OfflineRepository(context).getFeeds(context) + repository.getFeeds(context) + repository.close() if (!offlineFeeds.isNullOrEmpty()) { _feeds.value = offlineFeeds!! } @@ -70,10 +72,12 @@ } } if (offlineFeeds.isNullOrEmpty() && error != null) { _error.value = error!! - } else{ + } else { joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds -> _feeds.value = joinedFeeds - OfflineRepository(context).saveFeedCache(context, joinedFeeds) + val repository = OfflineRepository(context) + repository.saveFeedCache(context, joinedFeeds) + repository.close() } } }