Bimba.git

commit cdb6664dadbe705fe67908e0ac37bceaf47cb488

Author: Adam Evyčędo <git@apiote.xyz>

notify only about changed items in lists

use DiffUtil to calculate changes in RecyclerViews

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
index 1dafc8cacf61a3b221df3fd18941b1e9a567089c..5a1468c35018ec56820e55c0129b1983e8aaf534 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
@@ -11,6 +11,11 @@ import org.yaml.snakeyaml.Yaml
 import xyz.apiote.bimba.czwek.api.structs.VehicleStatusV1
 import xyz.apiote.fruchtfleisch.Reader
 import java.io.InputStream
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
 import kotlin.reflect.KClass
 
 class TrafficFormatException(override val message: String) : IllegalArgumentException()
@@ -195,6 +200,16 @@ 				reader.readI8(),
 				reader.readString()
 			)
 		}
+	}
+
+	fun toDateTime(): ZonedDateTime {
+		return ZonedDateTime.of(
+			LocalDateTime.of(
+				LocalDate.now().plusDays(DayOffset.toLong()),
+				LocalTime.of(Hour.toInt(), Minute.toInt(), Second.toInt())
+			),
+			ZoneId.of(Zone)
+		)
 	}
 }
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
index a14f717b8f64732d422520d96d8930ad0dd6e9ca..9a17d965b1b7fdfe1eb3dc8669aff6e9ac3d7421 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
@@ -14,6 +14,7 @@ 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 com.google.android.material.bottomsheet.BottomSheetDialog
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -28,6 +29,9 @@ import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dpToPixelI
 import xyz.apiote.bimba.czwek.repo.Departure
 import xyz.apiote.bimba.czwek.repo.Vehicle
+import java.time.Duration
+import java.time.ZoneId
+import java.time.ZonedDateTime
 import java.util.*
 
 
@@ -71,6 +75,30 @@ 	private var departures: List,
 	private val onClickListener: ((Departure) -> Unit)
 ) :
 	RecyclerView.Adapter<BimbaDepartureViewHolder>() {
+	private var lastUpdate: ZonedDateTime =
+		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
+
+	inner class DiffUtilCallback(
+		private val oldDepartures: List<Departure>,
+		private val newDepartures: List<Departure>
+	) : DiffUtil.Callback() {
+		override fun getOldListSize() = oldDepartures.size
+
+		override fun getNewListSize() = newDepartures.size
+
+		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
+			oldDepartures[oldItemPosition].ID == newDepartures[newItemPosition].ID
+
+		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+			val oldDeparture = oldDepartures[oldItemPosition]
+			val newDeparture = newDepartures[newItemPosition]
+			return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign &&
+				Duration.between(lastUpdate, oldDeparture.time.toDateTime()) == Duration.between(
+				ZonedDateTime.now(),
+				oldDeparture.time.toDateTime()
+			)
+		}
+	}
 
 	private var departuresPositions: MutableMap<String, Int> = HashMap()
 
@@ -100,16 +128,17 @@ 			departures[position]
 		}
 	}
 
-	@SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil
 	fun update(departures: List<Departure>) {
 		val newPositions: MutableMap<String, Int> = HashMap()
 		departures.forEachIndexed { i, departure ->
 			newPositions[departure.ID] = i
 		}
+		val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures))
 
 		this.departures = departures
 		departuresPositions = newPositions
-		notifyDataSetChanged()
+		lastUpdate = ZonedDateTime.now()
+		diff.dispatchUpdatesTo(this)
 	}
 }
 
@@ -266,7 +295,7 @@ 			content.apply {
 				findViewById<MapView>(R.id.map).let { map ->
 					map.setTileSource(TileSourceFactory.MAPNIK)
 					if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
-										and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
+							and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
 					) {
 						map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
 					}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
index 50b8d5ba35251105301364b3f2a27eef9804c80b..c59ee9c8f563016082f1d8fa69b1f379cd2e8e30 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
@@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.search
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.view.LayoutInflater
@@ -12,6 +11,7 @@ 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
@@ -105,7 +105,7 @@ 					R.string.line_headsigns,
 					line.headsigns[0].joinToString { it },
 					line.headsigns[1].joinToString { it })
 			}
-			holder?.description?.contentDescription =if (line.headsigns.size == 1) {
+			holder?.description?.contentDescription = if (line.headsigns.size == 1) {
 				context?.getString(
 					R.string.line_headsign_content_description,
 					line.headsigns[0].joinToString { it })
@@ -126,6 +126,66 @@ 	private val context: Context?,
 	private var queryables: List<Queryable>,
 ) :
 	RecyclerView.Adapter<BimbaViewHolder>() {
+	class DiffUtilCallback(
+		private val oldQueryables: List<Queryable>,
+		private val newQueryables: List<Queryable>
+	) : DiffUtil.Callback() {
+		override fun getOldListSize() = oldQueryables.size
+
+		override fun getNewListSize() = newQueryables.size
+
+		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+			val oldQueryable = oldQueryables[oldItemPosition]
+			val newQueryable = newQueryables[newItemPosition]
+			return if (oldQueryable::class.java != newQueryable::class.java) {
+				false
+			} else {
+				when (oldQueryable) {
+					is Line -> {
+						assert(newQueryable is Line)
+						oldQueryable.name == (newQueryable as Line).name // TODO compare line.ID when struct is updated
+					}
+
+					is Stop -> {
+						assert(newQueryable is Stop)
+						oldQueryable.code == (newQueryable as Stop).code
+					}
+
+					else -> false // XXX unreachable
+				}
+			}
+		}
+
+		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+			val oldQueryable = oldQueryables[oldItemPosition]
+			val newQueryable = newQueryables[newItemPosition]
+			return when (oldQueryable) {
+				is Line -> {
+					assert(newQueryable is Line)
+					val oldHeadsigns =
+						oldQueryable.headsigns.joinToString { hsList -> hsList.joinToString { it } }
+					val newHeadsigns =
+						(newQueryable as Line).headsigns.joinToString { hsList -> hsList.joinToString { it } }
+
+					oldQueryable.name == newQueryable.name && oldQueryable.type == newQueryable.type &&
+						oldQueryable.colour == newQueryable.colour && oldHeadsigns == newHeadsigns
+				}
+
+				is Stop -> {
+					assert(newQueryable is Stop)
+					val oldChangeOptions =
+						oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" }
+					val newChangeOptions =
+						(newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" }
+					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions
+				}
+
+				else -> false // XXX unreachable
+			}
+		}
+
+	}
+
 	private val onClickListener: ((Queryable) -> Unit) = {
 		when (it) {
 			is Stop -> {
@@ -158,10 +218,11 @@ 	}
 
 	override fun getItemCount(): Int = queryables.size
 
-	@SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil
 	fun update(queryables: List<Queryable>?) {
-		this.queryables = queryables ?: emptyList()
-		notifyDataSetChanged()
+		val newQueryables = queryables ?: emptyList()
+		val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.queryables, newQueryables))
+		this.queryables = newQueryables
+		diff.dispatchUpdatesTo(this)
 	}
 
 	fun click(position: Int) {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt
index 0ba9ada8523ff57245310e2197ac5d2476485d1f..e12daa5c56560c84de23ba21bc3cd3ef267ef5b1 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt
@@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.settings.feeds
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.DialogInterface
 import android.os.Bundle
@@ -12,6 +11,7 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import com.google.android.material.divider.MaterialDivider
@@ -74,6 +74,29 @@ 	private val onClickListener: ((String) -> Unit),
 	private val onEnabledChangedListener: ((String, Boolean) -> Unit)
 ) :
 	RecyclerView.Adapter<BimbaFeedInfoViewHolder>() {
+
+	class DiffUtilCallback(
+		private val oldFeeds: List<FeedInfo>,
+		private val oldSettings: FeedsSettings,
+		private val newFeeds: List<FeedInfo>,
+		private val newSettings: FeedsSettings
+	) : DiffUtil.Callback() {
+		override fun getOldListSize() = oldFeeds.size
+
+		override fun getNewListSize() = newFeeds.size
+
+		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
+			oldFeeds[oldItemPosition].id == newFeeds[newItemPosition].id
+
+		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+			val oldItem = oldFeeds[oldItemPosition]
+			val oldSetting = oldSettings.settings[oldItem.id]
+			val newItem = newFeeds[newItemPosition]
+			val newSetting = newSettings.settings[newItem.id]
+			return oldItem.cached == newItem.cached && oldItem.name == newItem.name && oldSetting?.enabled == newSetting?.enabled
+		}
+	}
+
 	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaFeedInfoViewHolder {
 		val rowView = inflater.inflate(R.layout.feedinfo, parent, false)
 		return BimbaFeedInfoViewHolder(rowView)
@@ -97,8 +120,14 @@ 	fun getItemPosition(feedID: String): Int {
 		return feeds.indexOfFirst { it.id == feedID }
 	}
 
-	@SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil
 	fun update(items: List<FeedInfo>?, settings: FeedsSettings?, notify: Boolean) {
+		val diffCallback = DiffUtilCallback(
+			feeds,
+			feedsSettings,
+			items?.sortedBy { it.name } ?: feeds,
+			settings ?: feedsSettings
+		)
+		val diff = DiffUtil.calculateDiff(diffCallback)
 		if (items != null) {
 			feeds = items.sortedBy { it.name }
 		}
@@ -106,7 +135,7 @@ 		if (settings != null) {
 			feedsSettings = settings
 		}
 		if (notify) {
-			notifyDataSetChanged()
+			diff.dispatchUpdatesTo(this)
 		}
 	}
 }