Bimba.git

commit acdf042a5ffbcc075ed152d550b9b0dcedbbd948

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()
 				}
 			}
 		}