Bimba.git

commit ae0b98018aed0a925cda41beada1ec6bd1f27de5

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

support offline feed settings

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


diff --git a/app/build.gradle b/app/build.gradle
index f383e96e7700b05f0146a3eb1a5d314d37c9a1a5..87a703a70ac8b7d7748fc9a3266cba2be6a994fa 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -61,7 +61,7 @@     implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
     implementation 'androidx.core:core-splashscreen:1.0.1'
     implementation 'com.google.openlocationcode:openlocationcode:1.0.4'
-    implementation 'org.osmdroid:osmdroid-android:6.1.16'
+    implementation 'org.osmdroid:osmdroid-android:6.1.18'
     implementation 'org.yaml:snakeyaml:2.2'
     implementation 'androidx.activity:activity-ktx:1.8.2'
     implementation 'com.google.openlocationcode:openlocationcode:1.0.4'




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
index 8f9260fce714586c54352e6d9d1b61603ad996e1..abf0ba7c8cb21b07c003e68828ba78ab11ef6e3b 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
@@ -20,8 +20,8 @@ 	val lastUpdate: LocalDate,
 	val qrHost: String,
 	val qrIn: QrLocation,
 	val qrSelector: String,
-	val validSince: LocalDate,
-	val validTill: LocalDate,
+	val validSince: LocalDate?,
+	val validTill: LocalDate?,
 	val cached: Boolean
 ) {
 	constructor(f: FeedInfoV2, cached: Boolean = false) : this(
@@ -47,8 +47,8 @@ 		f.lastUpdate.toLocalDate(),
 		"",
 		QrLocation.UNKNOWN,
 		"",
-		LocalDate.MIN,
-		LocalDate.MIN,
+		null,
+		null,
 		cached
 	)
 
@@ -74,8 +74,8 @@ 		if (lastUpdate.isAfter(other.lastUpdate)) lastUpdate else other.lastUpdate,
 		other.qrHost,
 		other.qrIn,
 		other.qrSelector,
-		if (validSince.isAfter(other.validSince)) validSince else other.validSince,
-		if (validTill.isAfter(other.validTill)) validTill else other.validTill,
+		if (validSince == null || validSince.isAfter(other.validSince)) validSince else other.validSince,
+		if (validTill == null || validTill.isAfter(other.validTill)) validTill else other.validTill,
 		this.cached && other.cached
 	)
 }




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 f5c784bd6ea8733475970f88cd3cd38e803c3ad7..750cf0d4bc662994915fad370e5db855265362c8 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
@@ -30,9 +30,26 @@ 			return emptyMap()
 		}
 		return when (val response =
 			withContext(Dispatchers.IO) { FeedsResponse.unmarshal(FileInputStream(file)) }) {
-			is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
-			is FeedsResponseV2 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
-			is FeedsResponseV1 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
+			is FeedsResponseDev -> response.feeds.associate {
+				Pair(
+					it.id,
+					FeedInfo(it).copy(cached = true)
+				)
+			}
+
+			is FeedsResponseV2 -> response.feeds.associate {
+				Pair(
+					it.id,
+					FeedInfo(it).copy(cached = true)
+				)
+			}
+
+			is FeedsResponseV1 -> response.feeds.associate {
+				Pair(
+					it.id,
+					FeedInfo(it).copy(cached = true)
+				)
+			}
 
 			else -> null
 		}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
index 82ea79773bd1938aa7eec2658d0f5c613e79b7c7..2642ac78ca1681506a386eba7c3b4d7a86bea3ab 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
@@ -24,6 +24,9 @@ import xyz.apiote.bimba.czwek.repo.OnlineRepository
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.repo.join
 
+// TODO on internet connection -> getServer
+// TODO swipe to refresh?
+
 class FeedChooserActivity : AppCompatActivity() {
 	private var _binding: ActivityFeedChooserBinding? = null
 	private val binding get() = _binding!!
@@ -31,7 +34,7 @@
 	private lateinit var adapter: BimbaFeedInfoAdapter
 
 	private val feeds: MutableMap<String, FeedInfo> = mutableMapOf()
-	private val feedsSettings = FeedsSettings.load(this, Server.get(this).apiPath)
+	private lateinit var feedsSettings: FeedsSettings
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
@@ -39,6 +42,7 @@ 		_binding = ActivityFeedChooserBinding.inflate(layoutInflater)
 		setContentView(binding.root)
 
 		migrateFeedsSettings(this)
+		feedsSettings = FeedsSettings.load(this, Server.get(this).apiPath)
 
 		setUpRecycler()
 		getServer()
@@ -55,6 +59,7 @@ 			BimbaFeedInfoAdapter(
 				layoutInflater,
 				feeds.map { it.value }.sortedBy { it.name },
 				feedsSettings,
+				this,
 				{ feedID ->
 					FeedBottomSheet(feeds[feedID]!!, feedsSettings.settings[feedID]) {
 						if (it != null) {
@@ -71,7 +76,6 @@ 							enabled = false,
 							useOnline = true
 						)
 					updateItems(null, feedsSettings, false)
-					adapter.notifyItemChanged(adapter.getItemPosition(feedID))
 				})
 		binding.resultsRecycler.adapter = adapter
 	}
@@ -79,21 +83,24 @@
 	private fun getServer() {
 		binding.progress.visibility = View.VISIBLE
 		binding.resultsRecycler.visibility = View.GONE
-
-		migrateFeedsSettings(this)
 
 		MainScope().launch {
 			val offlineRepository = OfflineRepository()
 			val offlineFeeds =
 				offlineRepository.getFeeds(Server.get(this@FeedChooserActivity), this@FeedChooserActivity)
 			if (!offlineFeeds.isNullOrEmpty()) {
+				feeds.putAll(offlineFeeds)
 				updateItems(offlineFeeds.map { it.value }, null)
 			}
 			try {
 				val repository = OnlineRepository()
 				val onlineFeeds =
 					repository.getFeeds(Server.get(this@FeedChooserActivity), this@FeedChooserActivity)
-				updateItems(joinFeeds(offlineFeeds, onlineFeeds).map { it.value }, null)
+				feeds.clear()
+				joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds ->
+					feeds.putAll(joinedFeeds)
+					updateItems(joinedFeeds.map { it.value }, null)
+				}
 			} catch (e: TrafficResponseException) {
 				if (offlineFeeds.isNullOrEmpty()) {
 					showError(e.error.imageResource, e.error.stringResource)
@@ -157,7 +164,7 @@ 		binding.feedsOverlay.visibility = View.GONE
 		binding.resultsRecycler.visibility = View.VISIBLE
 		binding.button.visibility = View.VISIBLE
 		adapter.update(feeds, feedsSettings, notify)
-		if (feeds.isNullOrEmpty()) {
+		if (feeds?.isEmpty() == true) {
 			showError(R.drawable.error_search, R.string.error_404)
 		}
 	}




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 4c5f8b8b5d9a5ed2ace5fc9888517b2435867db2..0ba9ada8523ff57245310e2197ac5d2476485d1f 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
@@ -5,6 +5,7 @@
 package xyz.apiote.bimba.czwek.settings.feeds
 
 import android.annotation.SuppressLint
+import android.content.Context
 import android.content.DialogInterface
 import android.os.Bundle
 import android.view.LayoutInflater
@@ -15,8 +16,10 @@ import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import com.google.android.material.divider.MaterialDivider
 import com.google.android.material.materialswitch.MaterialSwitch
+import com.google.android.material.textview.MaterialTextView
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.repo.FeedInfo
+import java.time.format.DateTimeFormatter
 
 
 class BimbaFeedInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -30,10 +33,20 @@ 		fun bind(
 			feed: FeedInfo,
 			feedSettings: FeedSettings?,
 			holder: BimbaFeedInfoViewHolder?,
+			context: Context,
 			onClickListener: (String) -> Unit,
 			onCheckedChangeListener: (String, Boolean) -> Unit
 		) {
-			// TODO update bottom sheet
+			val colorAttr = if (feed.cached) {
+				com.google.android.material.R.attr.colorOnSurfaceVariant
+			} else {
+				com.google.android.material.R.attr.colorOnSurface
+			}
+			holder?.name?.setTextColor(
+				context.theme.obtainStyledAttributes(
+					R.style.Theme_Bimba, intArrayOf(colorAttr)
+				).getColor(0, 0)
+			)
 			holder?.root?.setOnClickListener {
 				onClickListener(feed.id)
 			}
@@ -56,6 +69,7 @@ class BimbaFeedInfoAdapter(
 	private val inflater: LayoutInflater,
 	private var feeds: List<FeedInfo>,
 	private var feedsSettings: FeedsSettings,
+	private val context: Context,
 	private val onClickListener: ((String) -> Unit),
 	private val onEnabledChangedListener: ((String, Boolean) -> Unit)
 ) :
@@ -71,6 +85,7 @@ 		BimbaFeedInfoViewHolder.bind(
 			feed,
 			feedsSettings.settings[feed.id],
 			holder,
+			context,
 			onClickListener,
 			onEnabledChangedListener
 		)
@@ -98,7 +113,7 @@ }
 
 class FeedBottomSheet(
 	private var feed: FeedInfo,
-	private val settings: FeedSettings?,
+	private var settings: FeedSettings?,
 	private val onDismiss: (FeedSettings?) -> Unit
 ) :
 	BottomSheetDialogFragment() {
@@ -112,11 +127,36 @@ 		container: ViewGroup?,
 		savedInstanceState: Bundle?
 	): View {
 		val content = inflater.inflate(R.layout.feed_bottom_sheet, container, false)
-		content.findViewById<TextView>(R.id.title).text = feed.name
-		content.findViewById<TextView>(R.id.description).text = feed.description
-		content.findViewById<TextView>(R.id.attribution).text = feed.attribution
-		content.findViewById<TextView>(R.id.update_time).text =
+		content.findViewById<MaterialTextView>(R.id.title).text = feed.name
+		content.findViewById<MaterialTextView>(R.id.description).text = feed.description
+		content.findViewById<MaterialTextView>(R.id.outdated_info_warning).visibility =
+			if (feed.cached) {
+				View.VISIBLE
+			} else {
+				View.GONE
+			}
+		if (feed.validSince != null && feed.validTill != null) {
+			content.findViewById<MaterialTextView>(R.id.timetable_validity).apply {
+				visibility = View.VISIBLE
+				text = getString(
+					R.string.current_timetable_validity,
+					feed.validSince!!.format(DateTimeFormatter.ISO_LOCAL_DATE),
+					feed.validTill!!.format(DateTimeFormatter.ISO_LOCAL_DATE)
+				)
+			}
+		}
+		content.findViewById<MaterialTextView>(R.id.attribution).text = feed.attribution
+		content.findViewById<MaterialTextView>(R.id.update_time).text =
 			getString(R.string.last_update, feed.formatDate())
+		content.findViewById<MaterialSwitch>(R.id.onlineSwitch).apply {
+			isChecked = settings?.useOnline ?: false
+			setOnCheckedChangeListener { _, isChecked ->
+				settings = settings?.copy(useOnline = isChecked) ?: FeedSettings(
+					enabled = true,
+					useOnline = isChecked
+				)
+			}
+		}
 		return content
 	}
 




diff --git a/app/src/main/res/layout/feed_bottom_sheet.xml b/app/src/main/res/layout/feed_bottom_sheet.xml
index c1822e07f849202c3f3bb1674337293a4c8e75d8..bcb47b534c804342efbcdece3b2dee15fa746216 100644
--- a/app/src/main/res/layout/feed_bottom_sheet.xml
+++ b/app/src/main/res/layout/feed_bottom_sheet.xml
@@ -33,32 +33,91 @@ 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="parent"
 		tool:text="Poznań ZTM" />
 
+	<com.google.android.material.materialswitch.MaterialSwitch
+		android:id="@+id/onlineSwitch"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginEnd="16dp"
+		app:layout_constraintBottom_toBottomOf="@+id/onlineSwitchLabel"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toTopOf="@+id/onlineSwitchLabel" />
+
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/onlineSwitchLabel"
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="16dp"
+		android:layout_marginTop="16dp"
+		android:layout_marginEnd="8dp"
+		android:labelFor="@id/onlineSwitch"
+		android:text="@string/use_online_feed"
+		android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
+		app:layout_constraintEnd_toStartOf="@+id/onlineSwitch"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/title" />
+
+	<com.google.android.material.divider.MaterialDivider
+		android:id="@+id/onlineOfflineDivider"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		app:dividerInsetEnd="16dp"
+		app:dividerInsetStart="16dp"
+		android:layout_marginTop="16dp"
+		app:layout_constraintTop_toBottomOf="@+id/onlineSwitch" />
+
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/outdated_info_warning"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="8dp"
+		android:text="@string/information_may_be_outdated"
+		android:textStyle="italic"
+		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/onlineOfflineDivider" />
+
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/description"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
-		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"
+		android:layout_marginTop="16dp"
 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/title"
+		app:layout_constraintTop_toBottomOf="@+id/outdated_info_warning"
 		tool:text="Feed for Poznań" />
 
 	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/timetable_validity"
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:textAlignment="center"
+		android:layout_marginEnd="8dp"
+		android:visibility="gone"
+		android:layout_marginTop="16dp"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
+		app:layout_constraintTop_toBottomOf="@+id/description"
+		tool:text="Current timetable valid: 2024-01-01 to 2024-02-01" />
+
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/attribution"
+		android:layout_marginTop="16dp"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
-		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"
 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/description"
+		app:layout_constraintTop_toBottomOf="@+id/timetable_validity"
 		tool:text="(c) Poznań" />
 
 	<com.google.android.material.textview.MaterialTextView




diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
index 55a9fc293bf994e148ad829cf70cdc43789e9b72..57d1b22fb8aa55e6da9dd91c87f6d22761b9517a 100644
--- a/app/src/main/res/layout/feedinfo.xml
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -32,7 +32,7 @@ 		android:id="@+id/feed_switch"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_marginTop="16dp"
-		android:visibility="gone"
+		android:visibility="invisible"
 		android:layout_marginEnd="16dp"
 		android:layout_marginBottom="16dp"
 		android:text=""
@@ -43,7 +43,7 @@
 	<com.google.android.material.divider.MaterialDivider
 		android:id="@+id/divider"
 		android:layout_width="1dp"
-		android:visibility="gone"
+		android:visibility="invisible"
 		android:layout_height="0dp"
 		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4ef13134bf545a274dab57a52410b86a6729f898..bb318f8b3b8560f527858737a74b1f5de2929e9d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -107,4 +107,7 @@ 	FLOSS public transport passenger companion; a timetable in your pocket.
 	<string name="website_button_description">link to website</string>
 	<string name="code_button_description">link to source code</string>
 	<string name="mastodon_button_description">link to Mastodon</string>
+	<string name="use_online_feed">Use online feed</string>
+	<string name="information_may_be_outdated">Information may be outdated</string>
+	<string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
 </resources>
\ No newline at end of file