Bimba.git

commit 62e0c1d69f4e7d1d4557870d82cf5fc18af85534

Author: Adam <git@apiote.xyz>

ammend

 app/src/main/java/ml/adamsprogs/bimba/api/Api.kt | 43 
 app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt | 51 +
 app/src/main/java/ml/adamsprogs/bimba/search/Results.kt | 142 +++
 app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt | 127 ++
 app/src/main/res/drawable/ic_circle_white_24dp.xml | 9 
 app/src/main/res/drawable/ic_square_white_24dp.xml | 5 
 app/src/main/res/drawable/ic_stop_black_24dp.xml | 5 
 app/src/main/res/layout/activity_results.xml | 43 
 app/src/main/res/layout/suggestion.xml | 46 


diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
new file mode 100644
index 0000000000000000000000000000000000000000..67be4d5e21984f6a8dbbf89f1f15195d84b20aad
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
@@ -0,0 +1,43 @@
+package ml.adamsprogs.bimba.api
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.InputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.net.URLEncoder
+
+suspend fun queryItems(query: String): InputStream {
+	return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("q" to query))
+}
+
+suspend fun locateItems(plusCode: String): InputStream {
+	return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("near" to plusCode))
+}
+
+@Suppress("BlockingMethodInNonBlockingContext")
+suspend fun request(
+	host: String,
+	feed: String,
+	resource: String,
+	params: Map<String, String>
+): InputStream {
+	return withContext(Dispatchers.IO) {
+		val url = URL(
+			"$host/api/$feed/$resource${
+				params.map {
+					"${it.key}=${
+						URLEncoder.encode(
+							it.value,
+							"utf-8"
+						)
+					}"
+				}.joinToString("&", "?")
+			}"
+		)
+		val c = (url.openConnection() as HttpURLConnection).apply {
+			setRequestProperty("X-Bimba-Token", "ef0179272e7270e1a2da1710a8ba24e1")
+		}
+		c.inputStream
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6cb63b72d138c72809b6d7fa963409d6a349d0ed
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
@@ -0,0 +1,51 @@
+package ml.adamsprogs.bimba.api
+
+import xyz.apiote.fruchtfleisch.Reader
+import java.io.InputStream
+
+interface ItemsResponse {
+	companion object {
+		fun unmarshall(stream: InputStream): ItemsResponse {
+			val reader = Reader(stream)
+			when (reader.readUInt()) {
+				0UL -> {
+					TODO("error response")
+				}
+				1UL -> {
+					return ItemsSuccess.unmarshall(stream)
+				}
+				else -> {
+					TODO("throw unknown tag")
+				}
+			}
+		}
+	}
+}
+
+data class ItemsSuccess(val items: List<Item>) : ItemsResponse {
+	companion object {
+		fun unmarshall(stream: InputStream): ItemsSuccess {
+			val items = mutableListOf<Item>()
+			val reader = Reader(stream)
+			val itemsNum = reader.readUInt()
+			for (i in 0UL until itemsNum) {
+				when (reader.readUInt()) {
+					0UL -> {
+						items.add(Stop.unmarshall(stream))
+					}
+					1UL -> {
+						items.add(Line.unmarshall(stream))
+					}
+					else -> {
+						TODO("throw unknown tag")
+					}
+				}
+			}
+			return ItemsSuccess(items)
+		}
+	}
+}
+
+data class Error(val message: String) : ItemsResponse, DeparturesResponse {
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2218389bd2574588d9b6f66944be39a319137f43
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt
@@ -0,0 +1,142 @@
+package ml.adamsprogs.bimba.search
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.recyclerview.widget.RecyclerView
+import com.mancj.materialsearchbar.adapter.SuggestionsAdapter
+import ml.adamsprogs.bimba.R
+import ml.adamsprogs.bimba.api.Item
+import ml.adamsprogs.bimba.api.Line
+import ml.adamsprogs.bimba.api.LineType
+import ml.adamsprogs.bimba.api.Stop
+
+class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+	val root: View = itemView.findViewById(R.id.suggestion)
+	val icon: ImageView = itemView.findViewById(R.id.suggestion_image)
+	val title: TextView = itemView.findViewById(R.id.suggestion_title)
+	val description: TextView = itemView.findViewById(R.id.suggestion_description)
+
+	companion object {
+		fun bind(
+			item: Item,
+			holder: BimbaViewHolder?,
+			context: Context?,
+			onClickListener: (Item) -> Unit
+		) {
+			when (item) {
+				is Stop -> {
+					holder?.icon?.setImageResource(R.drawable.ic_stop_black_24dp) // todo stop icon // todo dark theme colour
+					holder?.title?.text = "${item.name} [${item.code}]"
+					holder?.description?.text =
+						item.changeOptions.joinToString { "${it.line} » ${it.headsign}" }
+				}
+				is Line -> {
+					val icon = when (item.type) {
+						LineType.TRAM -> {
+							AppCompatResources.getDrawable(context!!, R.drawable.ic_circle_white_24dp)?.mutate()
+						}
+						LineType.BUS -> {
+							AppCompatResources.getDrawable(
+								context!!,
+								R.drawable.ic_square_white_24dp
+							)?.mutate()  // todo bigger and squircle
+						}
+						else -> {
+							null
+						}
+					}
+					holder?.icon?.apply {
+						setImageDrawable(icon)
+						colorFilter = null
+						Log.v("Colour", "${item.name}: ${item.colour}, ${item.colour.toString(16)}")
+						setColorFilter(item.colour, PorterDuff.Mode.SRC_IN)  // fixme check colours in TRAFFIC
+					}
+					holder?.title?.text = item.name
+					holder?.description?.text =
+						"${item.headsignsThere.joinToString { it }} «» ${item.headsignsBack.joinToString { it }}"
+				}
+				else -> TODO("Shouldn't happen")
+			}
+			holder?.root?.setOnClickListener {
+				onClickListener(item)
+			}
+			/*holder!!.root.setOnClickListener {
+				onSuggestionClickListener.onSuggestionClickListener(suggestion)
+			}
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+				holder.text.text = Html.fromHtml(suggestion.getBody(context), Html.FROM_HTML_MODE_LEGACY)
+			} else {
+				@Suppress("DEPRECATION")
+				holder.text.text = Html.fromHtml(suggestion.getBody(context))
+			}
+
+			holder.text.setTextColor(getColor(context, R.color.textDark))
+
+			val icon = getDrawable(suggestion.getIcon(), context)
+			icon.mutate()
+			icon.colorFilter = null
+			if (suggestion is StopSuggestion)
+				when (suggestion.zone) {
+					"A" -> icon.setColorFilter(getColor(context, R.color.zoneA), PorterDuff.Mode.SRC_IN)
+					"B" -> icon.setColorFilter(getColor(context, R.color.zoneB), PorterDuff.Mode.SRC_IN)
+					"C" -> icon.setColorFilter(getColor(context, R.color.zoneC), PorterDuff.Mode.SRC_IN)
+					else -> icon.setColorFilter(getColor(context, R.color.textDark), PorterDuff.Mode.SRC_IN)
+				}
+			else if (suggestion is LineSuggestion) {
+				icon.setColorFilter(suggestion.getColour(), PorterDuff.Mode.SRC_IN)
+				holder.icon.setBackgroundColor(suggestion.getBgColour())
+			}
+			holder.icon.setImageDrawable(icon)*/
+		}
+	}
+}
+
+class BimbaResultsAdapter(
+	private val inflater: LayoutInflater,
+	private val context: Context?,
+	private var items: List<Item>,
+	private val onClickListener: ((Item) -> Unit)
+) :
+	RecyclerView.Adapter<BimbaViewHolder>() {
+	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder {
+		val rowView = inflater.inflate(R.layout.suggestion, parent, false)
+		return BimbaViewHolder(rowView)
+	}
+
+	override fun onBindViewHolder(holder: BimbaViewHolder, position: Int) {
+		BimbaViewHolder.bind(items[position], holder, context, onClickListener)
+	}
+
+	override fun getItemCount(): Int = items.size
+
+	fun update(items: List<Item>) {
+		this.items = items
+		notifyDataSetChanged()
+	}
+}
+
+class BimbaSuggestionsAdapter(
+	inflater: LayoutInflater,
+	private val context: Context?,
+	private val onClickListener: ((Item) -> Unit)
+) :
+	SuggestionsAdapter<Item, BimbaViewHolder>(inflater) {
+
+	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder {
+		val rowView = layoutInflater.inflate(R.layout.suggestion, parent, false)
+		return BimbaViewHolder(rowView)
+	}
+
+	override fun getSingleViewHeight(): Int = 64
+
+	override fun onBindSuggestionHolder(item: Item, holder: BimbaViewHolder?, pos: Int) {
+		BimbaViewHolder.bind(item, holder, context, onClickListener)
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..57b5d1f384f54b620c1238774cf8a5846ab228f1
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
@@ -0,0 +1,127 @@
+package ml.adamsprogs.bimba.search
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.openlocationcode.OpenLocationCode
+import kotlinx.coroutines.*
+import ml.adamsprogs.bimba.departures.DeparturesActivity
+import ml.adamsprogs.bimba.api.*
+import ml.adamsprogs.bimba.databinding.ActivityResultsBinding
+import java.io.InputStream
+
+@SuppressLint("MissingPermission")
+class ResultsActivity : AppCompatActivity(), LocationListener {
+	enum class Mode {
+		MODE_LOCATION, MODE_SEARCH
+	}
+
+	private var _binding: ActivityResultsBinding? = null
+
+	private val binding get() = _binding!!
+
+	private lateinit var adapter: BimbaResultsAdapter
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		super.onCreate(savedInstanceState)
+		_binding = ActivityResultsBinding.inflate(layoutInflater)
+		setContentView(binding.root)
+		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
+		adapter = BimbaResultsAdapter(layoutInflater, this, listOf()) {
+			when (it) {
+				is Stop -> {
+					val intent = Intent(this, DeparturesActivity::class.java).apply {
+						putExtra("code", it.code)
+						putExtra("name", it.name)
+						// todo line, date, etc
+					}
+					startActivity(intent)
+				}
+				is Line -> {
+					TODO("start line graph actvity")
+				}
+			}
+		}
+		binding.resultsRecycler.adapter = adapter
+		setSupportActionBar(binding.topAppBar)
+
+		WindowCompat.setDecorFitsSystemWindows(window, false)
+
+		when (intent.extras?.get("mode")) {
+			Mode.MODE_LOCATION -> {
+				supportActionBar?.title = "Stops nearby"
+				locate()
+			}
+			Mode.MODE_SEARCH -> {
+				val query = intent.extras?.getString("query")!!
+				supportActionBar?.title = "Results for ‘$query’"
+				getItemsByQuery(query)
+			}
+		}
+	}
+
+	private fun locate() {  // todo also on back to this activity (onRestart?)
+		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
+		locationManager.requestLocationUpdates(
+			LocationManager.GPS_PROVIDER, 5000, 10f, this
+		)
+	}
+
+	override fun onLocationChanged(location: Location) {
+		val code = OpenLocationCode.encode(location.latitude, location.longitude)
+		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
+		locationManager.removeUpdates(this)
+		getItemsByLocation(code)
+	}
+
+	override fun onDestroy() {  // todo also on hide this activity (onPause?)
+		super.onDestroy()
+		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
+		locationManager.removeUpdates(this)
+	}
+
+	private fun getItemsByQuery(query: String) {
+		MainScope().launch {
+			val itemsStream = queryItems(query)
+			updateItems(unmarshallItemResponse(itemsStream))
+			Log.v("RESPONSE", "getItemsByQuery")
+		}
+	}
+
+	private fun getItemsByLocation(plusCode: String) {
+		MainScope().launch {
+			val itemsStream = locateItems(plusCode)
+			updateItems(unmarshallItemResponse(itemsStream))
+			Log.v("RESPONSE", "getItemsByLocation")
+		}
+	}
+
+	private fun updateItems(items: List<Item>) {
+		binding.resultsProgress.visibility = View.GONE
+		binding.resultsRecycler.visibility = View.VISIBLE
+		adapter.update(items)
+	}
+
+	private suspend fun unmarshallItemResponse(stream: InputStream): List<Item> {
+		return withContext(Dispatchers.IO) {
+			when (val response = ItemsResponse.unmarshall(stream)) {
+				is ItemsSuccess -> {
+					return@withContext response.items
+				}
+				else -> {
+					TODO("Error response")
+				}
+			}
+		}
+	}
+
+}
\ No newline at end of file




diff --git a/app/src/main/res/drawable/ic_circle_white_24dp.xml b/app/src/main/res/drawable/ic_circle_white_24dp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5aaede4f20494018750d6c4d5403633bdba10779
--- /dev/null
+++ b/app/src/main/res/drawable/ic_circle_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z" />
+</vector>




diff --git a/app/src/main/res/drawable/ic_square_white_24dp.xml b/app/src/main/res/drawable/ic_square_white_24dp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c3963cf6bf7715f31cfd1f5e9c5ebce6fcff0029
--- /dev/null
+++ b/app/src/main/res/drawable/ic_square_white_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    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="M6,6h12v12H6z"/>
+</vector>




diff --git a/app/src/main/res/drawable/ic_stop_black_24dp.xml b/app/src/main/res/drawable/ic_stop_black_24dp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..14756c18e895b90db0ddfeddf4d937b7471c367f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_stop_black_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    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="M2,12C2,6.48 6.48,2 12,2s10,4.48 10,10 -4.48,10 -10,10S2,17.52 2,12zM12,18c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6 -6,2.69 -6,6 2.69,6 6,6z"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_results.xml b/app/src/main/res/layout/activity_results.xml
new file mode 100644
index 0000000000000000000000000000000000000000..52fbb1021ffa1df3b2e05915a803979f8160c9fa
--- /dev/null
+++ b/app/src/main/res/layout/activity_results.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<androidx.constraintlayout.widget.ConstraintLayout
+		android:id="@+id/results_progress"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent">
+
+		<ProgressBar
+			style="?android:attr/progressBarStyle"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			app:layout_constraintBottom_toBottomOf="parent"
+			app:layout_constraintEnd_toEndOf="parent"
+			app:layout_constraintStart_toStartOf="parent"
+			app:layout_constraintTop_toTopOf="parent" />
+	</androidx.constraintlayout.widget.ConstraintLayout>
+
+	<com.google.android.material.appbar.AppBarLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:fitsSystemWindows="true"
+		app:liftOnScroll="true">
+
+		<com.google.android.material.appbar.MaterialToolbar
+			android:id="@+id/topAppBar"
+			android:layout_width="match_parent"
+			android:layout_height="?attr/actionBarSize"
+			app:title="@string/title_activity_results" />
+
+	</com.google.android.material.appbar.AppBarLayout>
+
+	<androidx.recyclerview.widget.RecyclerView
+		android:id="@+id/results_recycler"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+
+		android:visibility="gone"
+		app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/suggestion.xml b/app/src/main/res/layout/suggestion.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d1eb7758fad295a2a8d7fe4f1c54638ea9a67cef
--- /dev/null
+++ b/app/src/main/res/layout/suggestion.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	android:id="@+id/suggestion"
+	android:layout_width="match_parent"
+	android:layout_height="64dp">
+
+	<ImageView
+		android:id="@+id/suggestion_image"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:layout_marginTop="8dp"
+		android:layout_marginBottom="8dp"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent" />
+
+	<TextView
+		android:id="@+id/suggestion_title"
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
+		android:text=""
+		android:textAppearance="@style/Theme.Bimba.SearchResult.Title"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toEndOf="@+id/suggestion_image"
+		app:layout_constraintTop_toTopOf="parent" />
+
+	<TextView
+		android:id="@+id/suggestion_description"
+		style="@style/Theme.Bimba.SearchResult.Description"
+		android:layout_width="0dp"
+		android:layout_height="0dp"
+		android:layout_marginTop="4dp"
+		android:layout_marginEnd="8dp"
+		android:layout_marginBottom="8dp"
+		android:text=""
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="@+id/suggestion_title"
+		app:layout_constraintTop_toBottomOf="@+id/suggestion_title" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file