Bimba.git

commit f2735d52119584de5de937fb1e8452b3d664ed2d

Author: Adam <git@apiote.xyz>

select date in departures

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


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
index 8a942fd52c4c7aed749d85e8dc70c1953f7bdc22..453ebf9ef8476585c8c8f3d16175c2cf33ffba36 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
@@ -17,6 +17,8 @@ import java.net.HttpURLConnection
 import java.net.MalformedURLException
 import java.net.URL
 import java.net.URLEncoder
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
 
 // todo [3.2] constants
 // todo [3.2] split api files to classes files
@@ -126,10 +128,14 @@ 	cm: ConnectivityManager,
 	server: Server,
 	feedID: String,
 	stop: String,
+	date: LocalDate?,
 	line: String? = null,
 	limit: Int? = null
 ): Result {
 	val params = mutableMapOf("code" to stop)
+	if (date != null) {
+		params["date"] = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
+	}
 	if (line != null) {
 		params["line"] = line
 	}




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 04c4d0a1eab4505ce9237ecf6e031cf6c5beeb22..b1075c10180fb11455713a1a54b273f982d8872e 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
@@ -11,6 +11,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_MASK
 import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED
 import android.content.res.Configuration.UI_MODE_NIGHT_YES
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -47,6 +48,7 @@ 		fun bind(
 			departure: Departure,
 			holder: BimbaDepartureViewHolder?,
 			context: Context?,
+			showAsTime: Boolean,
 			onClickListener: (Departure) -> Unit
 		) {
 			holder?.root?.setOnClickListener {
@@ -63,7 +65,7 @@ 					R.string.departure_headsign_content_description,
 					departure.vehicle.Headsign
 				)
 
-			holder?.departureTime?.text = departure.statusText(context)
+			holder?.departureTime?.text = departure.statusText(context, showAsTime)
 		}
 	}
 }
@@ -78,6 +80,7 @@ 	RecyclerView.Adapter() {
 	var lastUpdate: ZonedDateTime =
 		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
 		private set
+	private var showAsTime: Boolean = false
 
 	inner class DiffUtilCallback(
 		private val oldDepartures: List<Departure>,
@@ -94,7 +97,7 @@ 		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 &&
-				oldDeparture.statusText(context, lastUpdate) == newDeparture.statusText(context)
+				oldDeparture.statusText(context, false, lastUpdate) == newDeparture.statusText(context, false)
 		}
 	}
 
@@ -112,7 +115,8 @@ 		return BimbaDepartureViewHolder(rowView)
 	}
 
 	override fun onBindViewHolder(holder: BimbaDepartureViewHolder, position: Int) {
-		BimbaDepartureViewHolder.bind(departures[position], holder, context, onClickListener)
+		Log.i("show as time?", "$showAsTime")
+		BimbaDepartureViewHolder.bind(departures[position], holder, context, showAsTime, onClickListener)
 	}
 
 	override fun getItemCount(): Int = departures.size
@@ -126,7 +130,8 @@ 			departures[position]
 		}
 	}
 
-	fun update(departures: List<Departure>, areNewObserved: Boolean = false) {
+	fun update(departures: List<Departure>, showAsTime: Boolean, areNewObserved: Boolean = false) {
+		this.showAsTime = showAsTime
 		val newPositions: MutableMap<String, Int> = HashMap()
 		departures.forEachIndexed { i, departure ->
 			newPositions[departure.ID] = i
@@ -142,7 +147,7 @@ 		diff.dispatchUpdatesTo(this)
 	}
 
 	fun refreshItems() {
-		update(this.departures)
+		update(this.departures, showAsTime)
 	}
 }
 




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 f47185c9f0a0948217cb80a2aa3dbbb6db37e310..cc2c30cb53c2df594b2e983e3b4444b136d9d8d5 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
@@ -10,7 +10,6 @@ import android.os.Handler
 import android.os.Looper
 import android.text.format.DateUtils
 import android.text.format.DateUtils.MINUTE_IN_MILLIS
-import android.util.Log
 import android.view.View
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.content.res.AppCompatResources
@@ -19,6 +18,7 @@ import androidx.core.view.WindowCompat
 import androidx.lifecycle.ViewModelProvider
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.datepicker.MaterialDatePicker
 import com.google.android.material.snackbar.Snackbar
 import kotlinx.coroutines.Runnable
 import xyz.apiote.bimba.czwek.R
@@ -26,6 +26,9 @@ import xyz.apiote.bimba.czwek.api.Error
 import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding
 import xyz.apiote.bimba.czwek.repo.Departure
 import xyz.apiote.bimba.czwek.repo.Stop
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
 import java.time.ZonedDateTime
 
 class DeparturesActivity : AppCompatActivity() {
@@ -42,6 +45,12 @@ 	private lateinit var snackbar: Snackbar
 
 	private lateinit var viewModel: DeparturesViewModel
 
+	private val datePicker =
+		MaterialDatePicker.Builder.datePicker().setTitleText("Select day of departures")
+			.setNegativeButtonText(R.string.clear_date_selection)
+			.build()
+	private var date: LocalDate? = null
+
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
 		_binding = ActivityDeparturesBinding.inflate(layoutInflater)
@@ -57,12 +66,32 @@ 		viewModel.error.observe(this) {
 			showError(it)
 		}
 
+		datePicker.addOnNegativeButtonClickListener {
+			date = null
+			getDepartures()  // TODO force clear and error
+		}
+		datePicker.addOnPositiveButtonClickListener {
+			date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault())
+				.toLocalDate()
+			getDepartures()  // TODO force clear and error
+		}
+
 		binding.collapsingLayout.apply {
 			title = getName()
 			val tf = ResourcesCompat.getFont(this@DeparturesActivity, R.font.yellowcircle8)
 			setCollapsedTitleTypeface(tf)
 			setExpandedTitleTypeface(tf)
 		}
+		binding.departuresAppBar.setOnMenuItemClickListener {
+			when (it.itemId) {
+				R.id.departures_calendar -> {
+					datePicker.show(supportFragmentManager, "datePicker")
+					true
+				}
+
+				else -> super.onOptionsItemSelected(it)
+			}
+		}
 
 		binding.departuresRecycler.layoutManager = LinearLayoutManager(this)
 		binding.departuresRecycler.addOnScrollListener(
@@ -121,7 +150,7 @@
 	fun getDepartures() {
 		adapter.refreshItems()
 		setupSnackbar()
-		viewModel.getDepartures(this, getLine())
+		viewModel.getDepartures(this, getLine(), date)
 		handler.removeCallbacks(runnable)
 		runnable = Runnable { getDepartures() }
 		handler.postDelayed(runnable, 30 * 1000)
@@ -130,12 +159,16 @@
 	private fun setupSnackbar() {
 		val lastUpdateAgo = ZonedDateTime.now().toEpochSecond() - adapter.lastUpdate.toEpochSecond()
 		if (lastUpdateAgo > 59 && adapter.lastUpdate.year != 0) {
-			snackbar.setText(getString(R.string.departures_snackbar,
-				DateUtils.getRelativeTimeSpanString(
-				adapter.lastUpdate.toEpochSecond() * 1000,
-				ZonedDateTime.now().toEpochSecond() * 1000,
-				MINUTE_IN_MILLIS,
-				DateUtils.FORMAT_ABBREV_RELATIVE))
+			snackbar.setText(
+				getString(
+					R.string.departures_snackbar,
+					DateUtils.getRelativeTimeSpanString(
+						adapter.lastUpdate.toEpochSecond() * 1000,
+						ZonedDateTime.now().toEpochSecond() * 1000,
+						MINUTE_IN_MILLIS,
+						DateUtils.FORMAT_ABBREV_RELATIVE
+					)
+				)
 			).show()
 		} else {
 			snackbar.dismiss()
@@ -149,13 +182,18 @@ 		binding.errorImage.visibility = View.VISIBLE
 		binding.errorText.visibility = View.VISIBLE
 
 		binding.errorText.text = getString(error.stringResource)
-		binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource))
+		binding.errorImage.setImageDrawable(
+			AppCompatResources.getDrawable(
+				this,
+				error.imageResource
+			)
+		)
 	}
 
 	private fun updateItems(departures: List<Departure>, stop: Stop) {
 		setupSnackbar()
 		binding.departuresProgress.visibility = View.GONE
-		adapter.update(departures, true)
+		adapter.update(departures, date != null, true)
 		binding.collapsingLayout.apply {
 			title = stop.name
 		}




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 57f6fb5bfc9505ba2262d3cfe16cf7e48345f627..e5a198517744559aa0d6cf9e64bb0231b01f4230 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
@@ -22,6 +22,7 @@ import xyz.apiote.bimba.czwek.repo.OnlineRepository
 import xyz.apiote.bimba.czwek.repo.QrLocation
 import xyz.apiote.bimba.czwek.repo.StopDepartures
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
+import java.time.LocalDate
 
 class DeparturesViewModel : ViewModel() {
 	private val _departures = MutableLiveData<StopDepartures>()
@@ -33,7 +34,7 @@ 	var allItemsRequested = false
 	private var feed: FeedInfo? = null
 	private lateinit var code: String
 
-	fun getDepartures(context: Context, line: String?) {
+	fun getDepartures(context: Context, line: String?, date: LocalDate?) {
 		MainScope().launch {
 			try {
 				if (feed == null) {
@@ -47,11 +48,12 @@ 					repository.getDepartures(
 						feed!!.id,
 						code,
 						line,
+						date,
 						context,
 						requestedItemsNumber
 					)
 				stopDepartures?.let {
-					if (stopDepartures.departures.isEmpty()) {
+					if (stopDepartures.departures.isEmpty()) {  // TODO other error for empty than not-found
 						val (string, image) = mapHttpError(404)
 						throw TrafficResponseException(404, "", Error(404, string, image))
 					}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
index 617515c72577de45af91523cd643eb14e07a9760..4294211626b90a8acd68bf77db242f52020bd789 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
@@ -18,6 +18,7 @@ import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
 import java.time.temporal.ChronoUnit
 
 enum class AlertCause {
@@ -119,12 +120,15 @@ 		Vehicle(d.vehicle),
 		d.boarding
 	)
 
-	fun statusText(context: Context?, at: ZonedDateTime? = null): String {
+	fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String {
 		val now = at ?: Instant.now().atZone(ZoneId.systemDefault())
 		val departureTime = ZonedDateTime.of(
 			now.year, now.monthValue, now.dayOfMonth,
 			time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt(), 0, ZoneId.of(time.Zone)
 		).plus(time.DayOffset.toLong(), ChronoUnit.DAYS)
+		if (showAsTime) {
+			return departureTime.format(DateTimeFormatter.ofPattern("HH:mm"))
+		}
 		var r = status.toUInt()
 		if (departureTime.isBefore(now) && r < 3u) {
 			r = 0u




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
index dc8bdba932bf7327316bd331ae92791002adce35..873a343c9e4316f433134cfda163a476c1e84896 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
@@ -8,6 +8,7 @@ import android.content.Context
 import android.graphics.drawable.Drawable
 import android.net.ConnectivityManager
 import xyz.apiote.bimba.czwek.api.Server
+import java.time.LocalDate
 
 interface Queryable
 interface Locatable {
@@ -26,6 +27,7 @@ 	suspend fun getDepartures(
 		feedID: String,
 		stop: String,
 		line: String?,
+		date: LocalDate?,
 		context: Context,
 		limit: Int?
 	): StopDepartures?




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 0a92c37cd563b5560cd44f559b7ff8ce787d82bb..df3306a663bfbe18efa4c1ef22d42981f00fe7bf 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
@@ -16,6 +16,7 @@ import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2
 import java.io.File
 import java.io.FileInputStream
 import java.net.URLEncoder
+import java.time.LocalDate
 
 class OfflineRepository : Repository {
 	override suspend fun getFeeds(
@@ -59,6 +60,7 @@ 	override suspend fun getDepartures(
 		feedID: String,
 		stop: String,
 		line: String?,
+		date: LocalDate?,
 		context: Context,
 		limit: Int?
 	): StopDepartures? {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
index e25943d37a5bb1d941a759ad6cd56a29f23b20ba..bdac1f0a6db1e288e14b923b1c6257369abcf06c 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
@@ -44,6 +44,7 @@ import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2
 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3
 import java.io.File
 import java.net.URLEncoder
+import java.time.LocalDate
 
 // todo [3.2] in Repository check if responses are BARE or HTML
 
@@ -87,12 +88,13 @@ 	override suspend fun getDepartures(
 		feedID: String,
 		stop: String,
 		line: String?,
+		date: LocalDate?,
 		context: Context,
 		limit: Int?
 	): StopDepartures? {
 		val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 		val result =
-			xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), feedID, stop, line, limit)
+			xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), feedID, stop, date, line, limit)
 		if (result.error != null) {
 			if (result.stream != null) {
 				val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }




diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml
new file mode 100644
index 0000000000000000000000000000000000000000..46e37e41f0d907e88910b18e012b37d092bf89d6
--- /dev/null
+++ b/app/src/main/res/drawable/calendar.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="?attr/colorOnSurface"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml
index d71bb1c201f10214475335f515a81489fd690a3b..cb7760a48a12afb83d48beef6462b72db547422d 100644
--- a/app/src/main/res/layout/activity_departures.xml
+++ b/app/src/main/res/layout/activity_departures.xml
@@ -74,6 +74,7 @@ 				android:id="@+id/departures_app_bar"
 				android:layout_width="match_parent"
 				android:layout_height="?attr/actionBarSize"
 				android:elevation="0dp"
+				app:menu="@menu/departures_menu"
 				app:layout_collapseMode="pin" />
 
 		</com.google.android.material.appbar.CollapsingToolbarLayout>




diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cea228aa205e42352b56fc464f32fdf84982fd96
--- /dev/null
+++ b/app/src/main/res/menu/departures_menu.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto">
+	<item
+		android:id="@+id/departures_calendar"
+		android:icon="@drawable/calendar"
+		app:showAsAction="always"
+		android:contentDescription="@string/title_select_date"
+		android:title="@string/title_select_date" />
+</menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8b0524c6126083cb284dacaa638ca95ba4efe2b3..dab2829c9c31bdbe31b3844513eb1d334283f95b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -115,4 +115,6 @@ 	filter localities
 	<string name="error_41">This locality is not supported by the server</string>
 	<string name="stop_from_qr_code">QR code stop</string>
     <string name="departures_snackbar">Last update: %1$s</string>
+	<string name="title_select_date">select date</string>
+	<string name="clear_date_selection">Clear</string>
 </resources>
\ No newline at end of file